From 225a12fcd2848ce91bbf103850ffa340a1a985b1 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 17 Oct 2014 10:14:23 +0100 Subject: [PATCH 001/709] up timeout to 6 mins --- app.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 1061760d..decbf43f 100644 --- a/app.coffee +++ b/app.coffee @@ -21,7 +21,7 @@ app.use Metrics.http.monitor(logger) # Compile requests can take longer than the default two # minutes (including file download time), so bump up the # timeout a bit. -TIMEOUT = threeMinutes = 3 * 60 * 1000 +TIMEOUT = 6 * 60 * 1000 app.use (req, res, next) -> req.setTimeout TIMEOUT res.setTimeout TIMEOUT From af8674511249fc8ff92af30b954b5e013629b453 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 17 Oct 2014 11:03:08 +0100 Subject: [PATCH 002/709] increase max compile to 4 mins --- app/coffee/RequestParser.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 66765fb1..d98ca824 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -1,6 +1,6 @@ module.exports = RequestParser = VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"] - MAX_TIMEOUT: 60 + MAX_TIMEOUT: 300 parse: (body, callback = (error, data) ->) -> response = {} From 7f9c9176a983c11abd20b019210bc62b5be75466 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 28 Oct 2014 12:07:17 +0000 Subject: [PATCH 003/709] Force mimetype of output files to be safe --- app.coffee | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index c79eb923..6bc412b5 100644 --- a/app.coffee +++ b/app.coffee @@ -4,6 +4,8 @@ logger = require "logger-sharelatex" logger.initialize("clsi") smokeTest = require "smoke-test-sharelatex" +Path = require "path" + Metrics = require "metrics-sharelatex" Metrics.initialize("clsi") Metrics.open_sockets.monitor(logger) @@ -33,7 +35,13 @@ app.delete "/project/:project_id", CompileController.clearCache app.get "/project/:project_id/sync/code", CompileController.syncFromCode app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf -staticServer = express.static(Settings.path.compilesDir) +staticServer = express.static Settings.path.compilesDir, setHeaders: (res, path, stat) -> + if Path.basename(path) == "output.pdf" + res.set("Content-Type", "application/pdf") + else + # Force plain treatment of other file types to prevent hosting of HTTP/JS files + # that could be used in same-origin/XSS attacks. + res.set("Content-Type", "text/plain") app.get "/project/:project_id/output/*", (req, res, next) -> req.url = "/#{req.params.project_id}/#{req.params[0]}" staticServer(req, res, next) From b418ea201b843b445cfa050b32272037cb11aa44 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 29 Oct 2014 10:59:32 +0000 Subject: [PATCH 004/709] Update acceptance tests for new knitr, and remove markdown --- .../fixtures/examples/knitr/output.pdf | Bin 43236 -> 43236 bytes .../markdown-included/chapters/chapter1.md | 17 ------------- .../examples/markdown-included/main.tex | 9 ------- .../examples/markdown-included/output.pdf | Bin 34051 -> 0 bytes .../examples/markdown-standalone/main.md | 23 ------------------ .../examples/markdown-standalone/output.pdf | Bin 95563 -> 0 bytes 6 files changed, 49 deletions(-) delete mode 100644 test/acceptance/fixtures/examples/markdown-included/chapters/chapter1.md delete mode 100644 test/acceptance/fixtures/examples/markdown-included/main.tex delete mode 100644 test/acceptance/fixtures/examples/markdown-included/output.pdf delete mode 100644 test/acceptance/fixtures/examples/markdown-standalone/main.md delete mode 100644 test/acceptance/fixtures/examples/markdown-standalone/output.pdf diff --git a/test/acceptance/fixtures/examples/knitr/output.pdf b/test/acceptance/fixtures/examples/knitr/output.pdf index 133becbe4bc1255aa65ea9005f4062a839c0caae..a1f93799e58349967efead8c4583661423ae3f23 100644 GIT binary patch delta 575 zcmaEIk?F}rrU|k2A1eOH>=Ma{D#>1;dp04YDB-}?z?-?BKUWLgYzVbF!XzUS`=hYp z$;TgZ?^irv6xp*rN zR#z%f7F&D2ZqGhNt!q(kMN!}8irK2Qu(w|j1<~9fJpV=<7{EhVS%8R})ot{Q!zW$b=wtD_8!BgRZV zLm9aro6hgw@*_sc__m0D^46)__Z~UwojPMK&;9SW&(9}YZ#|H zp0^MxPv&S&%-FKQ&9XTDX~(-4cDy35GWMKY7_(u|*?;<*&v$p9ICpAhvICc$teNoq zaF471SVQ(*`ZIZ>knm{0kn`E-m`MZSA^zC4)`oa<_OhqzyBe@3ERZS@8Bj-q(fq-}03@yR4qS zm2>ss4XTz(tp>j-4_~lD0G42+0>amy7-UaPf4A<+N*m)iU#Z?6xcn=Wvd{*52fn zo$|a(yyoB1kHwv5a)NDojs5O@6B5^-npkF3b3fnwf9r`W_NzCF2OC|!@i8>+|KmqH z_NbIDRJi5im6V<)vSf*M{1WR=LHp-wOl~oqYy3U5?}gv}<&92<&7OGZMchrhKmF$YqsEiUi&FYlBzvTv-Sb>D zXiGg~!}m)kCT|oH-mJ=Wa)G*mg`u&bfswh1QIsZ^zHfetOJYf?f`*Hgk%6HJLUOa{ y;_EEVmL@Ld1{SWCZjL6FmafJ|j?SirPR>rQZqAlQCWe;gb_zCxluXuNu@wMv9|N@j diff --git a/test/acceptance/fixtures/examples/markdown-included/chapters/chapter1.md b/test/acceptance/fixtures/examples/markdown-included/chapters/chapter1.md deleted file mode 100644 index 920d9be7..00000000 --- a/test/acceptance/fixtures/examples/markdown-included/chapters/chapter1.md +++ /dev/null @@ -1,17 +0,0 @@ -Section Title -------------- - -* List item one -* List item two - -: Sample grid table. - -+---------------+---------------+--------------------+ -| Fruit | Price | Advantages | -+===============+===============+====================+ -| Bananas | $1.34 | - built-in wrapper | -| | | - bright color | -+---------------+---------------+--------------------+ -| Oranges | $2.10 | - cures scurvy | -| | | - tasty | -+---------------+---------------+--------------------+ diff --git a/test/acceptance/fixtures/examples/markdown-included/main.tex b/test/acceptance/fixtures/examples/markdown-included/main.tex deleted file mode 100644 index bfda9624..00000000 --- a/test/acceptance/fixtures/examples/markdown-included/main.tex +++ /dev/null @@ -1,9 +0,0 @@ -\documentclass{article} -\usepackage{longtable} -\usepackage{booktabs, multicol, multirow} - -\begin{document} - -\input{chapters/chapter1} - -\end{document} diff --git a/test/acceptance/fixtures/examples/markdown-included/output.pdf b/test/acceptance/fixtures/examples/markdown-included/output.pdf deleted file mode 100644 index 81a1136e80d7998772ba056439ecb7aedd4f2383..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34051 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 NHsDfKb@g}S0sy_uxYYmv diff --git a/test/acceptance/fixtures/examples/markdown-standalone/main.md b/test/acceptance/fixtures/examples/markdown-standalone/main.md deleted file mode 100644 index 036d971c..00000000 --- a/test/acceptance/fixtures/examples/markdown-standalone/main.md +++ /dev/null @@ -1,23 +0,0 @@ -% Title -% Author -% Date - -Chapter title -============= - -Section Title -------------- - -Hello world. Have a nice table: - -: Sample grid table. - -+---------------+---------------+--------------------+ -| Fruit | Price | Advantages | -+===============+===============+====================+ -| Bananas | $1.34 | - built-in wrapper | -| | | - bright color | -+---------------+---------------+--------------------+ -| Oranges | $2.10 | - cures scurvy | -| | | - tasty | -+---------------+---------------+--------------------+ diff --git a/test/acceptance/fixtures/examples/markdown-standalone/output.pdf b/test/acceptance/fixtures/examples/markdown-standalone/output.pdf deleted file mode 100644 index 9991ab82aefe9b86c25eee886e971bbf2e5acb7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95563 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 From fc674370bdf5267f8287cc68d2743278f9df0306 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 27 Nov 2014 16:11:00 +0000 Subject: [PATCH 005/709] respect the status code on the error if it exists --- app.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index c79eb923..f9b7760a 100644 --- a/app.coffee +++ b/app.coffee @@ -62,7 +62,7 @@ app.get "/health_check", (req, res)-> app.use (error, req, res, next) -> logger.error err: error, "server error" - res.send 500 + res.send err?.statusCode || 500 app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) -> logger.log "CLSI listening on #{host}:#{port}" From b4f0da0c42587fb6a3499e55dafd014ab69b1af7 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 27 Nov 2014 16:19:01 +0000 Subject: [PATCH 006/709] err != error --- app.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 56fd9bfc..5f55fdfb 100644 --- a/app.coffee +++ b/app.coffee @@ -70,7 +70,7 @@ app.get "/health_check", (req, res)-> app.use (error, req, res, next) -> logger.error err: error, "server error" - res.send err?.statusCode || 500 + res.send error?.statusCode || 500 app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) -> logger.log "CLSI listening on #{host}:#{port}" From 6bf8c22d78c49c886f5906a20e2b610305527d6a Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 2 Dec 2014 14:30:13 +0000 Subject: [PATCH 007/709] send a strong etag for the output.pdf file, needed for byte ranges in pdf.js --- app.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app.coffee b/app.coffee index 5f55fdfb..0d9904dc 100644 --- a/app.coffee +++ b/app.coffee @@ -38,10 +38,17 @@ app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf staticServer = express.static Settings.path.compilesDir, setHeaders: (res, path, stat) -> if Path.basename(path) == "output.pdf" res.set("Content-Type", "application/pdf") + # Calculate an etag in the same way as nginx + # https://github.com/tj/send/issues/65 + etag = (path, stat) -> + '"' + Math.ceil(+stat.mtime / 1000).toString(16) + + '-' + Number(stat.size).toString(16) + '"' + res.set("Etag", etag(path, stat)) else # Force plain treatment of other file types to prevent hosting of HTTP/JS files # that could be used in same-origin/XSS attacks. res.set("Content-Type", "text/plain") + app.get "/project/:project_id/output/*", (req, res, next) -> req.url = "/#{req.params.project_id}/#{req.params[0]}" staticServer(req, res, next) From 94397854c688947021a0482fd67d14006a6e8d0f Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 4 Dec 2014 21:37:09 +0000 Subject: [PATCH 008/709] Add in missing error check --- app/coffee/OutputFileFinder.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index c04edbc2..6df37758 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -9,7 +9,8 @@ module.exports = OutputFileFinder = for resource in resources incomingResources[resource.path] = true - OutputFileFinder._getAllFiles directory, (error, allFiles) -> + OutputFileFinder._getAllFiles directory, (error, allFiles = []) -> + return callback(error) if error? jobs = [] outputFiles = [] for file in allFiles From 5b2031b84feb4d02a21a14b61337b68bec8f4869 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 4 Dec 2014 22:07:37 +0000 Subject: [PATCH 009/709] Check file is not a symlink before returning it --- app.coffee | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app.coffee b/app.coffee index 0d9904dc..fe9ad532 100644 --- a/app.coffee +++ b/app.coffee @@ -5,6 +5,7 @@ logger.initialize("clsi") smokeTest = require "smoke-test-sharelatex" Path = require "path" +fs = require "fs" Metrics = require "metrics-sharelatex" Metrics.initialize("clsi") @@ -49,9 +50,26 @@ staticServer = express.static Settings.path.compilesDir, setHeaders: (res, path, # that could be used in same-origin/XSS attacks. res.set("Content-Type", "text/plain") + + app.get "/project/:project_id/output/*", (req, res, next) -> - req.url = "/#{req.params.project_id}/#{req.params[0]}" - staticServer(req, res, next) + basePath = Path.resolve("#{Settings.path.compilesDir}/#{req.params.project_id}") + path = Path.normalize("#{basePath}/#{req.params[0]}") + if path.slice(0, basePath.length) != basePath + logger.warn path: req.params[0], project_id: req.params.project_id, "trying to leave project directory, aborting" + res.send(404) + return + fs.lstat path, (error, stats) -> + if error? + if error.code == "ENOENT" + error.statusCode = 404 + return next(error) + if stats.isSymbolicLink() + error = new Error("file is a symlink") + error.statusCode = 404 + return next(error) + req.url = "/#{req.params.project_id}/#{req.params[0]}" + staticServer(req, res, next) app.get "/status", (req, res, next) -> res.send "CLSI is alive\n" From 92338ab419fef9a545a3c1d8a45d50aa9df37b50 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 4 Dec 2014 23:54:22 +0000 Subject: [PATCH 010/709] replaced old symlink logic with tested middlewear based on fs.realpath --- app.coffee | 23 +------ app/coffee/SymlinkCheckerMiddlewear.coffee | 17 ++++++ .../SymlinkCheckerMiddlewearTests.coffee | 60 +++++++++++++++++++ 3 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 app/coffee/SymlinkCheckerMiddlewear.coffee create mode 100644 test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee diff --git a/app.coffee b/app.coffee index fe9ad532..44d31575 100644 --- a/app.coffee +++ b/app.coffee @@ -50,26 +50,9 @@ staticServer = express.static Settings.path.compilesDir, setHeaders: (res, path, # that could be used in same-origin/XSS attacks. res.set("Content-Type", "text/plain") - - -app.get "/project/:project_id/output/*", (req, res, next) -> - basePath = Path.resolve("#{Settings.path.compilesDir}/#{req.params.project_id}") - path = Path.normalize("#{basePath}/#{req.params[0]}") - if path.slice(0, basePath.length) != basePath - logger.warn path: req.params[0], project_id: req.params.project_id, "trying to leave project directory, aborting" - res.send(404) - return - fs.lstat path, (error, stats) -> - if error? - if error.code == "ENOENT" - error.statusCode = 404 - return next(error) - if stats.isSymbolicLink() - error = new Error("file is a symlink") - error.statusCode = 404 - return next(error) - req.url = "/#{req.params.project_id}/#{req.params[0]}" - staticServer(req, res, next) +app.get "/project/:project_id/output/*", require("./app/js/SymlinkCheckerMiddlewear"), (req, res, next) -> + req.url = "/#{req.params.project_id}/#{req.params[0]}" + staticServer(req, res, next) app.get "/status", (req, res, next) -> res.send "CLSI is alive\n" diff --git a/app/coffee/SymlinkCheckerMiddlewear.coffee b/app/coffee/SymlinkCheckerMiddlewear.coffee new file mode 100644 index 00000000..b7baf1fa --- /dev/null +++ b/app/coffee/SymlinkCheckerMiddlewear.coffee @@ -0,0 +1,17 @@ +Path = require("path") +fs = require("fs") +Settings = require("settings-sharelatex") +logger = require("logger-sharelatex") + + +module.exports = (req, res, next)-> + basePath = Path.resolve("#{Settings.path.compilesDir}/#{req.params.project_id}") + requestedFsPath = Path.normalize("#{basePath}/#{req.params[0]}") + fs.realpath requestedFsPath, (err, realFsPath)-> + if err? + return res.send(500) + else if requestedFsPath != realFsPath + logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" + return res.send(404) + else + return next() diff --git a/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee b/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee new file mode 100644 index 00000000..50bd4271 --- /dev/null +++ b/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee @@ -0,0 +1,60 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../app/js/SymlinkCheckerMiddlewear" +expect = require("chai").expect + +describe "SymlinkCheckerMiddlewear", -> + + beforeEach -> + + @settings = + path: + compilesDir: "/compiles/here" + + @fs = {} + @SymlinkCheckerMiddlewear = SandboxedModule.require modulePath, requires: + "settings-sharelatex":@settings + "logger-sharelatex": + log:-> + warn:-> + "fs":@fs + @req = + params: + project_id:"12345" + + @res = {} + @req.params[0]= "output.pdf" + + + describe "sending a normal file through", -> + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/#{@req.params.project_id}/output.pdf") + + it "should call next", (done)-> + @SymlinkCheckerMiddlewear @req, @res, done + + + describe "with a symlink file", -> + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, null, "/etc/#{@req.params.project_id}/output.pdf") + + it "should send a 404", (done)-> + @res.send = (resCode)-> + resCode.should.equal 404 + done() + @SymlinkCheckerMiddlewear @req, @res + + describe "with an error from fs.realpath", -> + + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, "error") + + it "should send a 500", (done)-> + @res.send = (resCode)-> + resCode.should.equal 500 + done() + @SymlinkCheckerMiddlewear @req, @res + From ff94a76eb9709f9e36e62ce36af11ae817e76265 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 9 Dec 2014 11:07:58 +0000 Subject: [PATCH 011/709] Use find -type f to get a list of output files --- Gruntfile.coffee | 1 + app/coffee/OutputFileFinder.coffee | 57 ++++++++----------- test/unit/coffee/OutputFileFinderTests.coffee | 35 ++++++++---- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 90f9be16..09d37346 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -50,6 +50,7 @@ module.exports = (grunt) -> unit: options: reporter: "spec" + grep: grunt.option("grep") src: ["test/unit/js/**/*.js"] acceptance: options: diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index 6df37758..634b3421 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -1,7 +1,7 @@ async = require "async" fs = require "fs" Path = require "path" -wrench = require "wrench" +spawn = require("child_process").spawn module.exports = OutputFileFinder = findOutputFiles: (resources, directory, callback = (error, outputFiles) ->) -> @@ -16,44 +16,37 @@ module.exports = OutputFileFinder = for file in allFiles do (file) -> jobs.push (callback) -> - if incomingResources[file.path] + if incomingResources[file] return callback() else - OutputFileFinder._isDirectory Path.join(directory, file.path), (error, directory) -> - return callback(error) if error? - if !directory - outputFiles.push file - callback() + outputFiles.push { + path: file + type: file.match(/\.([^\.]+)$/)?[1] + } + callback() async.series jobs, (error) -> return callback(error) if error? callback null, outputFiles - _isDirectory: (path, callback = (error, directory) ->) -> - fs.stat path, (error, stat) -> - callback error, stat?.isDirectory() - - _getAllFiles: (directory, _callback = (error, outputFiles) ->) -> - callback = (error, outputFiles) -> - _callback(error, outputFiles) + _getAllFiles: (directory, _callback = (error, fileList) ->) -> + callback = (error, fileList) -> + _callback(error, fileList) _callback = () -> - outputFiles = [] - - wrench.readdirRecursive directory, (error, files) => - if error? - if error.code == "ENOENT" - # Directory doesn't exist, which is not a problem - return callback(null, []) - else - return callback(error) - - # readdirRecursive returns multiple times and finishes with a null response - if !files? - return callback(null, outputFiles) - - for file in files - outputFiles.push - path: file - type: file.match(/\.([^\.]+)$/)?[1] + proc = spawn("find", [directory, "-type", "f"]) + stdout = "" + proc.stdout.on "data", (chunk) -> + stdout += chunk.toString() + proc.on "error", callback + proc.on "close", (code) -> + if code != 0 + error = new Error("find returned non-zero exit code: #{code}") + return callback(error) + + fileList = stdout.trim().split("\n") + fileList = fileList.map (file) -> + # Strip leading directory + path = Path.relative(directory, file) + return callback null, fileList diff --git a/test/unit/coffee/OutputFileFinderTests.coffee b/test/unit/coffee/OutputFileFinderTests.coffee index b429bf1a..76adca4d 100644 --- a/test/unit/coffee/OutputFileFinderTests.coffee +++ b/test/unit/coffee/OutputFileFinderTests.coffee @@ -4,29 +4,25 @@ require('chai').should() modulePath = require('path').join __dirname, '../../../app/js/OutputFileFinder' path = require "path" expect = require("chai").expect +EventEmitter = require("events").EventEmitter describe "OutputFileFinder", -> beforeEach -> @OutputFileFinder = SandboxedModule.require modulePath, requires: "fs": @fs = {} - "wrench": @wrench = {} + "child_process": spawn: @spawn = sinon.stub() @directory = "/test/dir" @callback = sinon.stub() describe "findOutputFiles", -> beforeEach -> @resource_path = "resource/path.tex" - @output_paths = ["output.pdf", "extra", "extra/file.tex"] + @output_paths = ["output.pdf", "extra/file.tex"] + @all_paths = @output_paths.concat [@resource_path] @resources = [ path: @resource_path = "resource/path.tex" ] - @OutputFileFinder._isDirectory = (dirPath, callback = (error, directory) ->) => - callback null, dirPath == path.join(@directory, "extra") - - @wrench.readdirRecursive = (dir, callback) => - callback(null, [@resource_path].concat(@output_paths)) - callback(null, null) - sinon.spy @wrench, "readdirRecursive" + @OutputFileFinder._getAllFiles = sinon.stub().callsArgWith(1, null, @all_paths) @OutputFileFinder.findOutputFiles @resources, @directory, (error, @outputFiles) => it "should only return the output files, not directories or resource paths", -> @@ -37,5 +33,24 @@ describe "OutputFileFinder", -> path: "extra/file.tex", type: "tex" }] - + + describe "_getAllFiles", -> + beforeEach -> + @proc = new EventEmitter() + @proc.stdout = new EventEmitter() + @spawn.returns @proc + @directory = "/base/dir" + @OutputFileFinder._getAllFiles @directory, @callback + + @proc.stdout.emit( + "data", + ["/base/dir/main.tex", "/base/dir/chapters/chapter1.tex"].join("\n") + "\n" + ) + @proc.emit "close", 0 + + it "should call the callback with the relative file paths", -> + @callback.calledWith( + null, + ["main.tex", "chapters/chapter1.tex"] + ).should.equal true From 2c4fbd10ed7435c87c35aa4c5a0b6dd5b53c6077 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 9 Dec 2014 11:16:16 +0000 Subject: [PATCH 012/709] Add in some debugging logging --- app/coffee/OutputFileFinder.coffee | 8 +++++++- test/unit/coffee/OutputFileFinderTests.coffee | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index 634b3421..8ef06b6b 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -2,12 +2,15 @@ async = require "async" fs = require "fs" Path = require "path" spawn = require("child_process").spawn +logger = require "logger-sharelatex" module.exports = OutputFileFinder = findOutputFiles: (resources, directory, callback = (error, outputFiles) ->) -> incomingResources = {} for resource in resources incomingResources[resource.path] = true + + logger.log directory: directory, "getting output files" OutputFileFinder._getAllFiles directory, (error, allFiles = []) -> return callback(error) if error? @@ -33,8 +36,11 @@ module.exports = OutputFileFinder = callback = (error, fileList) -> _callback(error, fileList) _callback = () -> + + args = [directory, "-type", "f"] + logger.log args: args, "running find command" - proc = spawn("find", [directory, "-type", "f"]) + proc = spawn("find", args) stdout = "" proc.stdout.on "data", (chunk) -> stdout += chunk.toString() diff --git a/test/unit/coffee/OutputFileFinderTests.coffee b/test/unit/coffee/OutputFileFinderTests.coffee index 76adca4d..6acddf6b 100644 --- a/test/unit/coffee/OutputFileFinderTests.coffee +++ b/test/unit/coffee/OutputFileFinderTests.coffee @@ -11,6 +11,7 @@ describe "OutputFileFinder", -> @OutputFileFinder = SandboxedModule.require modulePath, requires: "fs": @fs = {} "child_process": spawn: @spawn = sinon.stub() + "logger-sharelatex": { log: sinon.stub() } @directory = "/test/dir" @callback = sinon.stub() From 84f3d3061d1fc3589ea9dd1fa67d857bbd7e451b Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 9 Dec 2014 11:25:23 +0000 Subject: [PATCH 013/709] Don't return error if directory doesn't exist yet --- app/coffee/OutputFileFinder.coffee | 5 +-- test/unit/coffee/OutputFileFinderTests.coffee | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index 8ef06b6b..fbfd38ed 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -47,9 +47,8 @@ module.exports = OutputFileFinder = proc.on "error", callback proc.on "close", (code) -> if code != 0 - error = new Error("find returned non-zero exit code: #{code}") - return callback(error) - + logger.warn {directory, code}, "find returned error, directory likely doesn't exist" + return callback null, [] fileList = stdout.trim().split("\n") fileList = fileList.map (file) -> # Strip leading directory diff --git a/test/unit/coffee/OutputFileFinderTests.coffee b/test/unit/coffee/OutputFileFinderTests.coffee index 6acddf6b..46d8c1fc 100644 --- a/test/unit/coffee/OutputFileFinderTests.coffee +++ b/test/unit/coffee/OutputFileFinderTests.coffee @@ -11,7 +11,7 @@ describe "OutputFileFinder", -> @OutputFileFinder = SandboxedModule.require modulePath, requires: "fs": @fs = {} "child_process": spawn: @spawn = sinon.stub() - "logger-sharelatex": { log: sinon.stub() } + "logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() } @directory = "/test/dir" @callback = sinon.stub() @@ -43,15 +43,26 @@ describe "OutputFileFinder", -> @directory = "/base/dir" @OutputFileFinder._getAllFiles @directory, @callback - @proc.stdout.emit( - "data", - ["/base/dir/main.tex", "/base/dir/chapters/chapter1.tex"].join("\n") + "\n" - ) - @proc.emit "close", 0 - - it "should call the callback with the relative file paths", -> - @callback.calledWith( - null, - ["main.tex", "chapters/chapter1.tex"] - ).should.equal true - + describe "successfully", -> + beforeEach -> + @proc.stdout.emit( + "data", + ["/base/dir/main.tex", "/base/dir/chapters/chapter1.tex"].join("\n") + "\n" + ) + @proc.emit "close", 0 + + it "should call the callback with the relative file paths", -> + @callback.calledWith( + null, + ["main.tex", "chapters/chapter1.tex"] + ).should.equal true + + describe "when the directory doesn't exist", -> + beforeEach -> + @proc.emit "close", 1 + + it "should call the callback with a blank array", -> + @callback.calledWith( + null, + [] + ).should.equal true From c84bd4fa3f13f1f267be2600729037a9c6538581 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Feb 2015 13:19:42 +0000 Subject: [PATCH 014/709] Release version 0.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2d4a669..1afcfeab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-clsi", "description": "A Node.js implementation of the CLSI LaTeX web-API", - "version": "0.1.1", + "version": "0.1.2", "repository": { "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" From 90cda12ed937b9d5fb5d694cd403d7684a6f2c4a Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 11 Feb 2015 12:03:36 +0000 Subject: [PATCH 015/709] Sanitize rootResourcePath --- app/coffee/RequestParser.coffee | 6 +++++- test/unit/coffee/RequestParserTests.coffee | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index d98ca824..93c843dd 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -27,10 +27,12 @@ module.exports = RequestParser = response.timeout = response.timeout * 1000 # milliseconds response.resources = (@_parseResource(resource) for resource in (compile.resources or [])) - response.rootResourcePath = @_parseAttribute "rootResourcePath", + + rootResourcePath = @_parseAttribute "rootResourcePath", compile.rootResourcePath default: "main.tex" type: "string" + response.rootResourcePath = RequestParser._sanitizePath(rootResourcePath) catch error return callback error @@ -72,3 +74,5 @@ module.exports = RequestParser = throw "Default not implemented" return attribute + _sanitizePath: (path) -> + path.replace(/[^a-zA-Z0-9_\-;.,\/ ]/g, "") \ No newline at end of file diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index 35ad6f4e..8545ff22 100644 --- a/test/unit/coffee/RequestParserTests.coffee +++ b/test/unit/coffee/RequestParserTests.coffee @@ -204,6 +204,13 @@ describe "RequestParser", -> @callback.calledWith("rootResourcePath attribute should be a string") .should.equal true - + describe "with a root resource path that needs escaping", -> + beforeEach -> + @validRequest.compile.rootResourcePath = "`rm -rf foo`.tex" + @RequestParser.parse @validRequest, @callback + @data = @callback.args[0][1] + + it "should return the escaped resource", -> + @data.rootResourcePath.should.equal "rm -rf foo.tex" From 1a7500f102e0ca7462baa6340974bb4cc4bf3bbc Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Feb 2015 11:21:35 +0000 Subject: [PATCH 016/709] Allow non-latin characters in the rootResourcePath --- app/coffee/RequestParser.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 93c843dd..d9a2e9bb 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -75,4 +75,5 @@ module.exports = RequestParser = return attribute _sanitizePath: (path) -> - path.replace(/[^a-zA-Z0-9_\-;.,\/ ]/g, "") \ No newline at end of file + # See http://php.net/manual/en/function.escapeshellcmd.php + path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\,\x0A\xFF]/g, "") \ No newline at end of file From f37004cec69817531800d000a7387eca9a8091b6 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 13 Feb 2015 11:28:43 +0000 Subject: [PATCH 017/709] update sanitizePath regex remove accidental inclusion of , and add null char \x00 --- app/coffee/RequestParser.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index d9a2e9bb..53268103 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -76,4 +76,4 @@ module.exports = RequestParser = _sanitizePath: (path) -> # See http://php.net/manual/en/function.escapeshellcmd.php - path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\,\x0A\xFF]/g, "") \ No newline at end of file + path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, "") From 1923352e66f103bd9f9304e8fde921fff9afd958 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 24 Feb 2015 14:40:05 +0000 Subject: [PATCH 018/709] save output files in a .cache directory --- app/coffee/CompileManager.coffee | 4 ++- app/coffee/OutputCacheManager.coffee | 49 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 app/coffee/OutputCacheManager.coffee diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 46e2c0f1..dbdb39fc 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -1,6 +1,7 @@ ResourceWriter = require "./ResourceWriter" LatexRunner = require "./LatexRunner" OutputFileFinder = require "./OutputFileFinder" +OutputCacheManager = require "./OutputCacheManager" Settings = require("settings-sharelatex") Path = require "path" logger = require "logger-sharelatex" @@ -32,7 +33,8 @@ module.exports = CompileManager = OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> return callback(error) if error? - callback null, outputFiles + OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> + callback null, newOutputFiles clearProject: (project_id, _callback = (error) ->) -> callback = (error) -> diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee new file mode 100644 index 00000000..0f96b11e --- /dev/null +++ b/app/coffee/OutputCacheManager.coffee @@ -0,0 +1,49 @@ +async = require "async" +fs = require "fs" +fse = require "fs-extra" +Path = require "path" +logger = require "logger-sharelatex" +_ = require "underscore" + +module.exports = OutputCacheManager = + CACHE_DIR: '.cache/clsi' + + saveOutputFiles: (outputFiles, target, callback) -> + # make a target/build_id directory and + # copy all the output files into it + buildId = 'build-' + Date.now() + relDir = OutputCacheManager.CACHE_DIR + '/' + buildId + newDir = target + '/' + relDir + OutputCacheManager.expireOutputFiles target + fse.ensureDir newDir, (err) -> + if err? + callback(err, outputFiles) + else + async.mapSeries outputFiles, (file, cb) -> + newFile = _.clone(file) + newFile.path = relDir + '/' + file.path + src = target + '/' + file.path + dst = target + '/' + newFile.path + #console.log 'src', src, 'dst', dst + fs.stat src, (err, stats) -> + if err? + cb(err) + else if stats.isFile() + #console.log 'isFile: copying' + fse.copy src, dst, (err) -> + cb(err, newFile) + else + # other filetype - shouldn't happen + cb(new Error("output file is not a file"), file) + , (err, results) -> + if err? + callback err, outputFiles + else + callback(err, results) + + expireOutputFiles: (target, callback) -> + # look in target for build dirs and delete if > N or age of mod time > T + cacheDir = target + '/' + OutputCacheManager.CACHE_DIR + fs.readdir cacheDir, (err, results) -> + console.log 'CACHEDIR', results + callback(err) if callback? From 67bfeacab81af032791253996fd30385aa8c41bf Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 24 Feb 2015 14:40:22 +0000 Subject: [PATCH 019/709] skip the cache directory when finding output files --- app/coffee/OutputFileFinder.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index fbfd38ed..26f07cb7 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -37,7 +37,7 @@ module.exports = OutputFileFinder = _callback(error, fileList) _callback = () -> - args = [directory, "-type", "f"] + args = [directory, "-name", ".cache", "-prune", "-o", "-type", "f", "-print"] logger.log args: args, "running find command" proc = spawn("find", args) From 163a33674b1472c887cf9f06de05a44211c30b78 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 24 Feb 2015 15:48:34 +0000 Subject: [PATCH 020/709] add an optimisation pass for the cached output files --- app/coffee/OutputCacheManager.coffee | 7 ++++++- app/coffee/OutputFileOptimiser.coffee | 30 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 app/coffee/OutputFileOptimiser.coffee diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 0f96b11e..479637d8 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -5,12 +5,16 @@ Path = require "path" logger = require "logger-sharelatex" _ = require "underscore" +OutputFileOptimiser = require "./OutputFileOptimiser" + module.exports = OutputCacheManager = CACHE_DIR: '.cache/clsi' saveOutputFiles: (outputFiles, target, callback) -> # make a target/build_id directory and # copy all the output files into it + # + # TODO: use Path module buildId = 'build-' + Date.now() relDir = OutputCacheManager.CACHE_DIR + '/' + buildId newDir = target + '/' + relDir @@ -31,7 +35,8 @@ module.exports = OutputCacheManager = else if stats.isFile() #console.log 'isFile: copying' fse.copy src, dst, (err) -> - cb(err, newFile) + OutputFileOptimiser.optimiseFile src, dst, (err, result) -> + cb(err, newFile) else # other filetype - shouldn't happen cb(new Error("output file is not a file"), file) diff --git a/app/coffee/OutputFileOptimiser.coffee b/app/coffee/OutputFileOptimiser.coffee new file mode 100644 index 00000000..a00dc205 --- /dev/null +++ b/app/coffee/OutputFileOptimiser.coffee @@ -0,0 +1,30 @@ +fs = require "fs" +Path = require "path" +spawn = require("child_process").spawn +logger = require "logger-sharelatex" + +module.exports = OutputFileOptimiser = + + optimiseFile: (src, dst, callback = (error) ->) -> + if src.match(/\.pdf$/) + OutputFileOptimiser.optimisePDF src, dst, callback + else + callback (null) + + optimisePDF: (src, dst, callback = (error) ->) -> + tmpOutput = dst + '.opt' + args = ["--linearize", src, tmpOutput] + logger.log args: args, "running qpdf command" + + proc = spawn("qpdf", args) + stdout = "" + proc.stdout.on "data", (chunk) -> + stdout += chunk.toString() + proc.on "error", callback + proc.on "close", (code) -> + if code != 0 + logger.warn {directory, code}, "qpdf returned error" + return callback null + fs.rename tmpOutput, dst, (err) -> + # could log an error here + callback null From b8cdd4fa85efec7e9eacded5b376d897417b9a26 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 24 Feb 2015 16:09:55 +0000 Subject: [PATCH 021/709] added package dependencies for caching --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1afcfeab..58e6745e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v1.0.0", "sqlite3": "~2.2.0", "express": "^4.2.0", - "body-parser": "^1.2.0" + "body-parser": "^1.2.0", + "fs-extra": "^0.16.3", + "underscore": "^1.8.2" }, "devDependencies": { "mocha": "1.10.0", From 151ea99639f1cd63246c4b637fdd5c10d1f33362 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 25 Feb 2015 17:05:19 +0000 Subject: [PATCH 022/709] accept build id parameter when serving static files --- app.coffee | 24 ++++++++++++++++++++++-- app/coffee/CompileController.coffee | 1 + app/coffee/CompileManager.coffee | 2 +- app/coffee/OutputCacheManager.coffee | 7 ++++--- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app.coffee b/app.coffee index 44d31575..dd2bae56 100644 --- a/app.coffee +++ b/app.coffee @@ -36,7 +36,24 @@ app.delete "/project/:project_id", CompileController.clearCache app.get "/project/:project_id/sync/code", CompileController.syncFromCode app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf -staticServer = express.static Settings.path.compilesDir, setHeaders: (res, path, stat) -> +url = require "url" + +staticForbidSymLinks = (root, options) -> + expressStatic = express.static root, options + basePath = Path.resolve(root) + return (req, res, next) -> + path = url.parse(req.url).pathname + requestedFsPath = Path.normalize("#{basePath}/#{path}") + fs.realpath requestedFsPath, (err, realFsPath)-> + if err? + return res.send(500) + else if requestedFsPath != realFsPath + logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" + return res.send(404) + else + expressStatic(req, res, next) + +staticServer = staticForbidSymLinks Settings.path.compilesDir, setHeaders: (res, path, stat) -> if Path.basename(path) == "output.pdf" res.set("Content-Type", "application/pdf") # Calculate an etag in the same way as nginx @@ -51,7 +68,10 @@ staticServer = express.static Settings.path.compilesDir, setHeaders: (res, path, res.set("Content-Type", "text/plain") app.get "/project/:project_id/output/*", require("./app/js/SymlinkCheckerMiddlewear"), (req, res, next) -> - req.url = "/#{req.params.project_id}/#{req.params[0]}" + if req.query?.build? && req.query.build.match(/^[0-9]+$/) + req.url = "/#{req.params.project_id}/.cache/clsi/#{req.query.build}/#{req.params[0]}" + else + req.url = "/#{req.params.project_id}/#{req.params[0]}" staticServer(req, res, next) app.get "/status", (req, res, next) -> diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index c7382020..29373c36 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -35,6 +35,7 @@ module.exports = CompileController = outputFiles: outputFiles.map (file) -> url: "#{Settings.apis.clsi.url}/project/#{request.project_id}/output/#{file.path}" type: file.type + build: file.build } clearCache: (req, res, next = (error) ->) -> diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index dbdb39fc..ac0c2411 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -33,7 +33,7 @@ module.exports = CompileManager = OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> return callback(error) if error? - OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> + OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> callback null, newOutputFiles clearProject: (project_id, _callback = (error) ->) -> diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 479637d8..c34a6628 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -15,7 +15,7 @@ module.exports = OutputCacheManager = # copy all the output files into it # # TODO: use Path module - buildId = 'build-' + Date.now() + buildId = Date.now() relDir = OutputCacheManager.CACHE_DIR + '/' + buildId newDir = target + '/' + relDir OutputCacheManager.expireOutputFiles target @@ -25,9 +25,8 @@ module.exports = OutputCacheManager = else async.mapSeries outputFiles, (file, cb) -> newFile = _.clone(file) - newFile.path = relDir + '/' + file.path src = target + '/' + file.path - dst = target + '/' + newFile.path + dst = target + '/' + relDir + '/' + file.path #console.log 'src', src, 'dst', dst fs.stat src, (err, stats) -> if err? @@ -36,6 +35,8 @@ module.exports = OutputCacheManager = #console.log 'isFile: copying' fse.copy src, dst, (err) -> OutputFileOptimiser.optimiseFile src, dst, (err, result) -> + console.log 'setting buildId on', newFile, 'to', buildId + newFile.build = buildId cb(err, newFile) else # other filetype - shouldn't happen From 81e85de1697a7e59af385ff5a1e98f97fbb3872d Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 26 Feb 2015 11:20:56 +0000 Subject: [PATCH 023/709] Release version 0.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1afcfeab..70283c66 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-clsi", "description": "A Node.js implementation of the CLSI LaTeX web-API", - "version": "0.1.2", + "version": "0.1.3", "repository": { "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" From e7ed8d786a2675dd35beae1696d48bf7a5f61356 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 26 Feb 2015 15:30:57 +0000 Subject: [PATCH 024/709] fix tests to allow for build parameter --- test/unit/coffee/CompileControllerTests.coffee | 3 +++ test/unit/coffee/CompileManagerTests.coffee | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index d2babf7c..bc2d9881 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -36,9 +36,11 @@ describe "CompileController", -> @output_files = [{ path: "output.pdf" type: "pdf" + build: 1234 }, { path: "output.log" type: "log" + build: 1234 }] @RequestParser.parse = sinon.stub().callsArgWith(1, null, @request) @ProjectPersistenceManager.markProjectAsJustAccessed = sinon.stub().callsArg(1) @@ -73,6 +75,7 @@ describe "CompileController", -> outputFiles: @output_files.map (file) => url: "#{@Settings.apis.clsi.url}/project/#{@project_id}/output/#{file.path}" type: file.type + build: file.build ) .should.equal true diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 5d110eb1..8923dea0 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -12,6 +12,7 @@ describe "CompileManager", -> "./LatexRunner": @LatexRunner = {} "./ResourceWriter": @ResourceWriter = {} "./OutputFileFinder": @OutputFileFinder = {} + "./OutputCacheManager": @OutputCacheManager = {} "settings-sharelatex": @Settings = { path: compilesDir: "/compiles/dir" } "logger-sharelatex": @logger = { log: sinon.stub() } "child_process": @child_process = {} @@ -26,6 +27,15 @@ describe "CompileManager", -> path: "output.pdf" type: "pdf" }] + @build_files = [{ + path: "output.log" + type: "log" + build: 1234 + }, { + path: "output.pdf" + type: "pdf" + build: 1234 + }] @request = resources: @resources = "mock-resources" rootResourcePath: @rootResourcePath = "main.tex" @@ -37,6 +47,7 @@ describe "CompileManager", -> @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArg(3) @LatexRunner.runLatex = sinon.stub().callsArg(2) @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) + @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) @CompileManager.doCompile @request, @callback it "should write the resources to disk", -> @@ -60,7 +71,8 @@ describe "CompileManager", -> .should.equal true it "should return the output files", -> - @callback.calledWith(null, @output_files).should.equal true + console.log 'output_files', @build_files + @callback.calledWith(null, @build_files).should.equal true describe "clearProject", -> describe "succesfully", -> From 280d64cf6029fd8349c710bc5a4d9d88a1bdc2ed Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 26 Feb 2015 15:32:01 +0000 Subject: [PATCH 025/709] remove debugging code --- app/coffee/OutputCacheManager.coffee | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index c34a6628..c869bfdb 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -27,15 +27,12 @@ module.exports = OutputCacheManager = newFile = _.clone(file) src = target + '/' + file.path dst = target + '/' + relDir + '/' + file.path - #console.log 'src', src, 'dst', dst fs.stat src, (err, stats) -> if err? cb(err) else if stats.isFile() - #console.log 'isFile: copying' fse.copy src, dst, (err) -> OutputFileOptimiser.optimiseFile src, dst, (err, result) -> - console.log 'setting buildId on', newFile, 'to', buildId newFile.build = buildId cb(err, newFile) else @@ -51,5 +48,4 @@ module.exports = OutputCacheManager = # look in target for build dirs and delete if > N or age of mod time > T cacheDir = target + '/' + OutputCacheManager.CACHE_DIR fs.readdir cacheDir, (err, results) -> - console.log 'CACHEDIR', results callback(err) if callback? From 198e1ef4928fbcf162795189918e6ed35227dd9b Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 27 Feb 2015 13:15:28 +0000 Subject: [PATCH 026/709] cleanup and logging --- app/coffee/OutputCacheManager.coffee | 115 +++++++++++++++++++------- app/coffee/OutputFileOptimiser.coffee | 11 ++- 2 files changed, 93 insertions(+), 33 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index c869bfdb..f77227a1 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -8,44 +8,101 @@ _ = require "underscore" OutputFileOptimiser = require "./OutputFileOptimiser" module.exports = OutputCacheManager = - CACHE_DIR: '.cache/clsi' + CACHE_SUBDIR: '.cache/clsi' + BUILD_REGEX: /^[0-9a-f]+$/ # build id is Date.now() converted to hex + CACHE_LIMIT: 32 # maximum of 32 cache directories + CACHE_AGE: 60*60*1000 # up to one hour old - saveOutputFiles: (outputFiles, target, callback) -> - # make a target/build_id directory and + path: (buildId) -> + # used by static server, given build id return '.cache/clsi/buildId' + return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId) + + saveOutputFiles: (outputFiles, compileDir, callback = (error) ->) -> + # make a compileDir/CACHE_SUBDIR/build_id directory and # copy all the output files into it - # - # TODO: use Path module - buildId = Date.now() - relDir = OutputCacheManager.CACHE_DIR + '/' + buildId - newDir = target + '/' + relDir - OutputCacheManager.expireOutputFiles target - fse.ensureDir newDir, (err) -> + cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR) + # Put the files into a new cache subdirectory + buildId = Date.now().toString(16) + cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId) + # let file expiry run in the background + OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId} + + checkFile = (src, callback) -> + # check if we have a valid file to copy into the cache + fs.stat src, (err, stats) -> + if err? + # some problem reading the file + logger.error err: err, file: src, "stat error for file in cache" + callback(err) + else if not stats.isFile() + # other filetype - reject it + logger.error err: err, src: src, dst: dst, stat: stats, "nonfile output - refusing to copy to cache" + callback(new Error("output file is not a file"), file) + else + # it's a plain file, ok to copy + callback(null) + + copyFile = (src, dst, callback) -> + # copy output file into the cache + fse.copy src, dst, (err) -> + if err? + logger.error err: err, src: src, dst: dst, "copy error for file in cache" + callback(err) + else + # call the optimiser for the file too + OutputFileOptimiser.optimiseFile src, dst, callback + + # make the new cache directory + fse.ensureDir cacheDir, (err) -> if err? + logger.error err: err, directory: cacheDir, "error creating cache directory" callback(err, outputFiles) else + # copy all the output files into the new cache directory async.mapSeries outputFiles, (file, cb) -> newFile = _.clone(file) - src = target + '/' + file.path - dst = target + '/' + relDir + '/' + file.path - fs.stat src, (err, stats) -> - if err? - cb(err) - else if stats.isFile() - fse.copy src, dst, (err) -> - OutputFileOptimiser.optimiseFile src, dst, (err, result) -> - newFile.build = buildId - cb(err, newFile) - else - # other filetype - shouldn't happen - cb(new Error("output file is not a file"), file) + [src, dst] = [Path.join(compileDir, file.path), Path.join(cacheDir, file.path)] + checkFile src, (err) -> + copyFile src, dst, (err) -> + if not err? + newFile.build = buildId # attach a build id if we cached the file + cb(err, newFile) , (err, results) -> if err? - callback err, outputFiles + # pass back the original files if we encountered *any* error + callback(err, outputFiles) else + # pass back the list of new files in the cache callback(err, results) - expireOutputFiles: (target, callback) -> - # look in target for build dirs and delete if > N or age of mod time > T - cacheDir = target + '/' + OutputCacheManager.CACHE_DIR - fs.readdir cacheDir, (err, results) -> - callback(err) if callback? + expireOutputFiles: (cacheRoot, options, callback = (error) ->) -> + # look in compileDir for build dirs and delete if > N or age of mod time > T + fs.readdir cacheRoot, (err, results) -> + if err? + logger.error err: err, project_id: cacheRoot, "error clearing cache" + return callback(err) + + dirs = results.sort().reverse() + currentTime = Date.now() + + isExpired = (dir, index) -> + return false if options?.keep == dir + # remove any directories over the hard limit + return true if index > OutputCacheManager.CACHE_LIMIT + # we can get the build time from the directory name + dirTime = parseInt(dir, 16) + age = currentTime - dirTime + return age > OutputCacheManager.CACHE_AGE + + toRemove = _.filter(dirs, isExpired) + + removeDir = (dir, cb) -> + fse.remove Path.join(cacheRoot, dir), (err, result) -> + logger.log cache: cacheRoot, dir: dir, "removed expired cache dir" + if err? + logger.error err: err, dir: dir, "cache remove error" + cb(err, result) + + async.eachSeries toRemove, (dir, cb) -> + removeDir dir, cb + , callback diff --git a/app/coffee/OutputFileOptimiser.coffee b/app/coffee/OutputFileOptimiser.coffee index a00dc205..d1e45d4f 100644 --- a/app/coffee/OutputFileOptimiser.coffee +++ b/app/coffee/OutputFileOptimiser.coffee @@ -6,6 +6,8 @@ logger = require "logger-sharelatex" module.exports = OutputFileOptimiser = optimiseFile: (src, dst, callback = (error) ->) -> + # check output file (src) and see if we can optimise it, storing + # the result in the build directory (dst) if src.match(/\.pdf$/) OutputFileOptimiser.optimisePDF src, dst, callback else @@ -19,12 +21,13 @@ module.exports = OutputFileOptimiser = proc = spawn("qpdf", args) stdout = "" proc.stdout.on "data", (chunk) -> - stdout += chunk.toString() - proc.on "error", callback + stdout += chunk.toString() + proc.on "error", callback proc.on "close", (code) -> if code != 0 logger.warn {directory, code}, "qpdf returned error" return callback null fs.rename tmpOutput, dst, (err) -> - # could log an error here - callback null + if err? + logger.warn {tmpOutput, dst}, "failed to rename output of qpdf command" + callback err From 0692e964ef4b45f06c32efcdc79a580b6f679386 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 27 Feb 2015 13:16:01 +0000 Subject: [PATCH 027/709] use OutputCacheManager to construct static path to files --- app.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app.coffee b/app.coffee index dd2bae56..ee2e8387 100644 --- a/app.coffee +++ b/app.coffee @@ -12,6 +12,7 @@ Metrics.initialize("clsi") Metrics.open_sockets.monitor(logger) ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager" +OutputCacheManager = require "./app/js/OutputCacheManager" require("./app/js/db").sync() @@ -68,8 +69,8 @@ staticServer = staticForbidSymLinks Settings.path.compilesDir, setHeaders: (res, res.set("Content-Type", "text/plain") app.get "/project/:project_id/output/*", require("./app/js/SymlinkCheckerMiddlewear"), (req, res, next) -> - if req.query?.build? && req.query.build.match(/^[0-9]+$/) - req.url = "/#{req.params.project_id}/.cache/clsi/#{req.query.build}/#{req.params[0]}" + if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX) + req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.query.build) + "/#{req.params[0]}" else req.url = "/#{req.params.project_id}/#{req.params[0]}" staticServer(req, res, next) From 37cc9f3715bd677ce4c59f985807759cdbb7abbd Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 27 Feb 2015 13:57:57 +0000 Subject: [PATCH 028/709] provide a static server which forbids symlinks prevents mismatch between rootdir of server and rootdir of symlink checking middleware --- app.coffee | 29 ++++++-------------- app/coffee/OutputCacheManager.coffee | 8 ++++-- app/coffee/StaticServerForbidSymlinks.coffee | 24 ++++++++++++++++ app/coffee/SymlinkCheckerMiddlewear.coffee | 17 ------------ 4 files changed, 39 insertions(+), 39 deletions(-) create mode 100644 app/coffee/StaticServerForbidSymlinks.coffee delete mode 100644 app/coffee/SymlinkCheckerMiddlewear.coffee diff --git a/app.coffee b/app.coffee index ee2e8387..9ac1d1dc 100644 --- a/app.coffee +++ b/app.coffee @@ -37,24 +37,12 @@ app.delete "/project/:project_id", CompileController.clearCache app.get "/project/:project_id/sync/code", CompileController.syncFromCode app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf -url = require "url" - -staticForbidSymLinks = (root, options) -> - expressStatic = express.static root, options - basePath = Path.resolve(root) - return (req, res, next) -> - path = url.parse(req.url).pathname - requestedFsPath = Path.normalize("#{basePath}/#{path}") - fs.realpath requestedFsPath, (err, realFsPath)-> - if err? - return res.send(500) - else if requestedFsPath != realFsPath - logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" - return res.send(404) - else - expressStatic(req, res, next) - -staticServer = staticForbidSymLinks Settings.path.compilesDir, setHeaders: (res, path, stat) -> +ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks" + +# create a static server which does not allow access to any symlinks +# avoids possible mismatch of root directory between middleware check +# and serving the files +staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHeaders: (res, path, stat) -> if Path.basename(path) == "output.pdf" res.set("Content-Type", "application/pdf") # Calculate an etag in the same way as nginx @@ -68,9 +56,10 @@ staticServer = staticForbidSymLinks Settings.path.compilesDir, setHeaders: (res, # that could be used in same-origin/XSS attacks. res.set("Content-Type", "text/plain") -app.get "/project/:project_id/output/*", require("./app/js/SymlinkCheckerMiddlewear"), (req, res, next) -> +app.get "/project/:project_id/output/*", (req, res, next) -> if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX) - req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.query.build) + "/#{req.params[0]}" + # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.query.build, "/#{req.params[0]}") else req.url = "/#{req.params.project_id}/#{req.params[0]}" staticServer(req, res, next) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index f77227a1..50c58370 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -13,9 +13,13 @@ module.exports = OutputCacheManager = CACHE_LIMIT: 32 # maximum of 32 cache directories CACHE_AGE: 60*60*1000 # up to one hour old - path: (buildId) -> + path: (buildId, file) -> # used by static server, given build id return '.cache/clsi/buildId' - return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId) + if buildId.match OutputCacheManager.BUILD_REGEX + return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file) + else + # for invalid build id, return top level + return file saveOutputFiles: (outputFiles, compileDir, callback = (error) ->) -> # make a compileDir/CACHE_SUBDIR/build_id directory and diff --git a/app/coffee/StaticServerForbidSymlinks.coffee b/app/coffee/StaticServerForbidSymlinks.coffee new file mode 100644 index 00000000..2f481908 --- /dev/null +++ b/app/coffee/StaticServerForbidSymlinks.coffee @@ -0,0 +1,24 @@ +Path = require("path") +fs = require("fs") +Settings = require("settings-sharelatex") +logger = require("logger-sharelatex") +url = require "url" + +module.exports = ForbidSymlinks = (staticFn, root, options) -> + expressStatic = staticFn root, options + basePath = Path.resolve(root) + return (req, res, next) -> + path = url.parse(req.url)?.pathname + requestedFsPath = Path.normalize("#{basePath}/#{path}") + fs.realpath requestedFsPath, (err, realFsPath)-> + if err? + logger.warn err:err, requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "error checking file access" + if err.code == 'ENOENT' + return res.sendStatus(404) + else + return res.sendStatus(500) + else if requestedFsPath != realFsPath + logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" + return res.sendStatus(404) + else + expressStatic(req, res, next) diff --git a/app/coffee/SymlinkCheckerMiddlewear.coffee b/app/coffee/SymlinkCheckerMiddlewear.coffee deleted file mode 100644 index b7baf1fa..00000000 --- a/app/coffee/SymlinkCheckerMiddlewear.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Path = require("path") -fs = require("fs") -Settings = require("settings-sharelatex") -logger = require("logger-sharelatex") - - -module.exports = (req, res, next)-> - basePath = Path.resolve("#{Settings.path.compilesDir}/#{req.params.project_id}") - requestedFsPath = Path.normalize("#{basePath}/#{req.params[0]}") - fs.realpath requestedFsPath, (err, realFsPath)-> - if err? - return res.send(500) - else if requestedFsPath != realFsPath - logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" - return res.send(404) - else - return next() From 916b4cb40b1dd34d68b2ee19d799152a1404ddbb Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 27 Feb 2015 15:38:57 +0000 Subject: [PATCH 029/709] move convert tests from middleware to restricted static server --- .../StaticServerForbidSymlinksTests.coffee | 82 +++++++++++++++++++ .../SymlinkCheckerMiddlewearTests.coffee | 60 -------------- 2 files changed, 82 insertions(+), 60 deletions(-) create mode 100644 test/unit/coffee/StaticServerForbidSymlinksTests.coffee delete mode 100644 test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee diff --git a/test/unit/coffee/StaticServerForbidSymlinksTests.coffee b/test/unit/coffee/StaticServerForbidSymlinksTests.coffee new file mode 100644 index 00000000..e17bacef --- /dev/null +++ b/test/unit/coffee/StaticServerForbidSymlinksTests.coffee @@ -0,0 +1,82 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../app/js/StaticServerForbidSymlinks" +expect = require("chai").expect + +describe "StaticServerForbidSymlinks", -> + + beforeEach -> + + @settings = + path: + compilesDir: "/compiles/here" + + @fs = {} + @ForbidSymlinks = SandboxedModule.require modulePath, requires: + "settings-sharelatex":@settings + "logger-sharelatex": + log:-> + warn:-> + "fs":@fs + + @dummyStatic = (rootDir, options) -> + return (req, res, next) -> + # console.log "dummyStatic serving file", rootDir, "called with", req.url + # serve it + next() + + @StaticServerForbidSymlinks = @ForbidSymlinks @dummyStatic, @settings.path.compilesDir + @req = + params: + project_id:"12345" + + @res = {} + @req.url = "/12345/output.pdf" + + + describe "sending a normal file through", -> + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/#{@req.params.project_id}/output.pdf") + + it "should call next", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 200 + done() + @StaticServerForbidSymlinks @req, @res, done + + + describe "with a missing file", -> + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, {code: 'ENOENT'}, "#{@settings.path.compilesDir}/#{@req.params.project_id}/unknown.pdf") + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + + + describe "with a symlink file", -> + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, null, "/etc/#{@req.params.project_id}/output.pdf") + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + + describe "with an error from fs.realpath", -> + + beforeEach -> + @fs.realpath = sinon.stub().callsArgWith(1, "error") + + it "should send a 500", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 500 + done() + @StaticServerForbidSymlinks @req, @res + diff --git a/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee b/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee deleted file mode 100644 index 50bd4271..00000000 --- a/test/unit/coffee/SymlinkCheckerMiddlewearTests.coffee +++ /dev/null @@ -1,60 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../app/js/SymlinkCheckerMiddlewear" -expect = require("chai").expect - -describe "SymlinkCheckerMiddlewear", -> - - beforeEach -> - - @settings = - path: - compilesDir: "/compiles/here" - - @fs = {} - @SymlinkCheckerMiddlewear = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": - log:-> - warn:-> - "fs":@fs - @req = - params: - project_id:"12345" - - @res = {} - @req.params[0]= "output.pdf" - - - describe "sending a normal file through", -> - beforeEach -> - @fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/#{@req.params.project_id}/output.pdf") - - it "should call next", (done)-> - @SymlinkCheckerMiddlewear @req, @res, done - - - describe "with a symlink file", -> - beforeEach -> - @fs.realpath = sinon.stub().callsArgWith(1, null, "/etc/#{@req.params.project_id}/output.pdf") - - it "should send a 404", (done)-> - @res.send = (resCode)-> - resCode.should.equal 404 - done() - @SymlinkCheckerMiddlewear @req, @res - - describe "with an error from fs.realpath", -> - - beforeEach -> - @fs.realpath = sinon.stub().callsArgWith(1, "error") - - it "should send a 500", (done)-> - @res.send = (resCode)-> - resCode.should.equal 500 - done() - @SymlinkCheckerMiddlewear @req, @res - From 3a4dd9df508b88876ba9d0be8c8eda920d125c98 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 27 Feb 2015 16:07:02 +0000 Subject: [PATCH 030/709] fix double callback for proc.on 'error' and proc.on 'close' --- app/coffee/OutputFileOptimiser.coffee | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/coffee/OutputFileOptimiser.coffee b/app/coffee/OutputFileOptimiser.coffee index d1e45d4f..d0c091d3 100644 --- a/app/coffee/OutputFileOptimiser.coffee +++ b/app/coffee/OutputFileOptimiser.coffee @@ -2,6 +2,7 @@ fs = require "fs" Path = require "path" spawn = require("child_process").spawn logger = require "logger-sharelatex" +_ = require "underscore" module.exports = OutputFileOptimiser = @@ -22,12 +23,15 @@ module.exports = OutputFileOptimiser = stdout = "" proc.stdout.on "data", (chunk) -> stdout += chunk.toString() - proc.on "error", callback + callback = _.once(callback) # avoid double call back for error and close event + proc.on "error", (err) -> + logger.warn {err, args}, "qpdf failed" + callback(null) # ignore the error proc.on "close", (code) -> if code != 0 - logger.warn {directory, code}, "qpdf returned error" - return callback null + logger.warn {code, args}, "qpdf returned error" + return callback(null) # ignore the error fs.rename tmpOutput, dst, (err) -> if err? logger.warn {tmpOutput, dst}, "failed to rename output of qpdf command" - callback err + callback(null) # ignore the error From 75ef0d6581a3d02e6f4cce96a39ba72a4964ebf3 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 2 Mar 2015 09:58:20 +0000 Subject: [PATCH 031/709] skip cache directory error when empty --- app/coffee/OutputCacheManager.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 50c58370..af4ce612 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -83,6 +83,7 @@ module.exports = OutputCacheManager = # look in compileDir for build dirs and delete if > N or age of mod time > T fs.readdir cacheRoot, (err, results) -> if err? + return callback(null) if err.code == 'ENOENT' # cache directory is empty logger.error err: err, project_id: cacheRoot, "error clearing cache" return callback(err) From 7551bc3135759bd70f42c1180aa6a962ec2c6fc9 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 2 Mar 2015 11:31:48 +0000 Subject: [PATCH 032/709] reduce cache limit for pdfs --- app/coffee/OutputCacheManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index af4ce612..66abc4ea 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -10,7 +10,7 @@ OutputFileOptimiser = require "./OutputFileOptimiser" module.exports = OutputCacheManager = CACHE_SUBDIR: '.cache/clsi' BUILD_REGEX: /^[0-9a-f]+$/ # build id is Date.now() converted to hex - CACHE_LIMIT: 32 # maximum of 32 cache directories + CACHE_LIMIT: 2 # maximum number of cache directories CACHE_AGE: 60*60*1000 # up to one hour old path: (buildId, file) -> From 65f2f23cf66aeacd1f8d61451fd4a9e960bad1a3 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 16 Mar 2015 15:02:45 +0000 Subject: [PATCH 033/709] add v8 profiler on /profile?time=MS url --- app.coffee | 9 +++++++++ package.json | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 9ac1d1dc..78fad79b 100644 --- a/app.coffee +++ b/app.coffee @@ -86,6 +86,15 @@ app.get "/health_check", (req, res)-> res.contentType(resCacher?.setContentType) res.send resCacher?.code, resCacher?.body +profiler = require "v8-profiler" +app.get "/profile", (req, res) -> + time = parseInt(req.query.time || "1000") + profiler.startProfiling("test") + setTimeout () -> + profile = profiler.stopProfiling("test") + res.json(profile) + , time + app.use (error, req, res, next) -> logger.error err: error, "server error" res.send error?.statusCode || 500 diff --git a/package.json b/package.json index 678d9712..97bd5ae9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "express": "^4.2.0", "body-parser": "^1.2.0", "fs-extra": "^0.16.3", - "underscore": "^1.8.2" + "underscore": "^1.8.2", + "v8-profiler": "^5.2.4" }, "devDependencies": { "mocha": "1.10.0", From 24e20a79f48c39ddd3bd314679cd9763132ac9b8 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 16 Mar 2015 16:47:14 +0000 Subject: [PATCH 034/709] remove unnecessary call to async.series in OutputFileFinder callback was previously async but is now synchronous, so high stack usage. --- app/coffee/OutputFileFinder.coffee | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index 26f07cb7..dcec5f5d 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -17,20 +17,12 @@ module.exports = OutputFileFinder = jobs = [] outputFiles = [] for file in allFiles - do (file) -> - jobs.push (callback) -> - if incomingResources[file] - return callback() - else - outputFiles.push { - path: file - type: file.match(/\.([^\.]+)$/)?[1] - } - callback() - - async.series jobs, (error) -> - return callback(error) if error? - callback null, outputFiles + if !incomingResources[file] + outputFiles.push { + path: file + type: file.match(/\.([^\.]+)$/)?[1] + } + callback null, outputFiles _getAllFiles: (directory, _callback = (error, fileList) ->) -> callback = (error, fileList) -> From a35df6d82942a574fa8a875e61ad51a63821e4dd Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 20 Mar 2015 15:25:02 +0000 Subject: [PATCH 035/709] Release version 0.1.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97bd5ae9..859a834f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-clsi", "description": "A Node.js implementation of the CLSI LaTeX web-API", - "version": "0.1.3", + "version": "0.1.4", "repository": { "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" From 143f948193a0584c99a51c79f6e4ce1ee401ff85 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 9 Apr 2015 14:40:02 +0100 Subject: [PATCH 036/709] add heapdump support for memory profiling --- app.coffee | 4 ++++ package.json | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 78fad79b..464ff390 100644 --- a/app.coffee +++ b/app.coffee @@ -95,6 +95,10 @@ app.get "/profile", (req, res) -> res.json(profile) , time +app.get "/heapdump", (req, res)-> + require('heapdump').writeSnapshot '/tmp/' + Date.now() + '.clsi.heapsnapshot', (err, filename)-> + res.send filename + app.use (error, req, res, next) -> logger.error err: error, "server error" res.send error?.statusCode || 500 diff --git a/package.json b/package.json index 859a834f..8bf90356 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "body-parser": "^1.2.0", "fs-extra": "^0.16.3", "underscore": "^1.8.2", - "v8-profiler": "^5.2.4" + "v8-profiler": "^5.2.4", + "heapdump": "^0.3.5" }, "devDependencies": { "mocha": "1.10.0", From 85c6c3fe2b4dd2ca55f91ea63fda2da5ee1b57b9 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 29 Apr 2015 15:54:08 +0100 Subject: [PATCH 037/709] log errors when copying files from cache --- app/coffee/UrlCache.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index 1836a4e7..153e133e 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -62,6 +62,8 @@ module.exports = UrlCache = _copyFile: (from, to, _callback = (error) ->) -> callbackOnce = (error) -> + if error? + logger.error err: error, from:from, to:to, "error copying file from cache" _callback(error) _callback = () -> writeStream = fs.createWriteStream(to) From 8db907c766c49cf0d7b5c25704aea4929cad53b7 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 29 Apr 2015 15:54:38 +0100 Subject: [PATCH 038/709] invalidate the cache if there is an error copying a file --- app/coffee/UrlCache.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index 153e133e..941bc9de 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -10,7 +10,12 @@ module.exports = UrlCache = downloadUrlToFile: (project_id, url, destPath, lastModified, callback = (error) ->) -> UrlCache._ensureUrlIsInCache project_id, url, lastModified, (error, pathToCachedUrl) => return callback(error) if error? - UrlCache._copyFile(pathToCachedUrl, destPath, callback) + UrlCache._copyFile pathToCachedUrl, destPath, (error) -> + if error? + UrlCache._clearUrlDetails project_id, url, () -> + callback(error) + else + callback(error) clearProject: (project_id, callback = (error) ->) -> UrlCache._findAllUrlsInProject project_id, (error, urls) -> From 651279b21f550ad476e5c1138e7fda349efc7b8d Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 29 Apr 2015 15:55:58 +0100 Subject: [PATCH 039/709] log errors when downloading files and clean up failed downloads --- app/coffee/UrlFetcher.coffee | 26 ++++++++++++++++++++++--- test/unit/coffee/UrlFetcherTests.coffee | 4 +++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.coffee index 66c5edf0..5082a159 100644 --- a/app/coffee/UrlFetcher.coffee +++ b/app/coffee/UrlFetcher.coffee @@ -1,15 +1,31 @@ request = require("request").defaults(jar: false) fs = require("fs") +logger = require "logger-sharelatex" module.exports = UrlFetcher = pipeUrlToFile: (url, filePath, _callback = (error) ->) -> callbackOnce = (error) -> - _callback(error) - _callback = () -> + cleanUp error, (error) -> + _callback(error) + _callback = () -> + + cleanUp = (error, callback) -> + if error? + logger.log filePath: filePath, "deleting file from cache due to error" + fs.unlink filePath, (err) -> + if err? + logger.err err: err, filePath: filePath, "error deleting file from cache" + callback(error) + else + callback() - urlStream = request.get(url) fileStream = fs.createWriteStream(filePath) + fileStream.on 'error', (error) -> + logger.error err: error, url:url, filePath: filePath, "error writing file into cache" + callbackOnce(error) + logger.log url:url, filePath: filePath, "downloading url to cache" + urlStream = request.get(url) urlStream.on "response", (res) -> if res.statusCode >= 200 and res.statusCode < 300 urlStream.pipe(fileStream) @@ -17,7 +33,11 @@ module.exports = UrlFetcher = callbackOnce(new Error("URL returned non-success status code: #{res.statusCode} #{url}")) urlStream.on "error", (error) -> + logger.error err: error, url:url, filePath: filePath, "error downloading url" callbackOnce(error or new Error("Something went wrong downloading the URL #{url}")) urlStream.on "end", () -> + # FIXME: what if we get an error writing the file? Maybe we + # should be using the fileStream end event as the point of + # callback. callbackOnce() diff --git a/test/unit/coffee/UrlFetcherTests.coffee b/test/unit/coffee/UrlFetcherTests.coffee index 3e6dc926..7d38aa66 100644 --- a/test/unit/coffee/UrlFetcherTests.coffee +++ b/test/unit/coffee/UrlFetcherTests.coffee @@ -11,6 +11,7 @@ describe "UrlFetcher", -> @UrlFetcher = SandboxedModule.require modulePath, requires: request: defaults: @defaults = sinon.stub().returns(@request = {}) fs: @fs = {} + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } it "should turn off the cookie jar in request", -> @defaults.calledWith(jar: false) @@ -21,7 +22,8 @@ describe "UrlFetcher", -> @path = "/path/to/file/on/disk" @request.get = sinon.stub().returns(@urlStream = new EventEmitter) @urlStream.pipe = sinon.stub() - @fs.createWriteStream = sinon.stub().returns(@fileStream = "write-stream-stub") + @fs.createWriteStream = sinon.stub().returns(@fileStream = { on: () -> }) + @fs.unlink = (file, callback) -> callback() @UrlFetcher.pipeUrlToFile(@url, @path, @callback) it "should request the URL", -> From 9892751ff617181d2cbd0e810c61b5d3d690e813 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 29 Apr 2015 15:58:28 +0100 Subject: [PATCH 040/709] prevent leak of urlStream on failed downloads --- app/coffee/UrlFetcher.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.coffee index 5082a159..e14285d5 100644 --- a/app/coffee/UrlFetcher.coffee +++ b/app/coffee/UrlFetcher.coffee @@ -30,6 +30,16 @@ module.exports = UrlFetcher = if res.statusCode >= 200 and res.statusCode < 300 urlStream.pipe(fileStream) else + logger.error statusCode: res.statusCode, url:url, filePath: filePath, "unexpected status code downloading url to cache" + # https://nodejs.org/api/http.html#http_class_http_clientrequest + # If you add a 'response' event handler, then you must consume + # the data from the response object, either by calling + # response.read() whenever there is a 'readable' event, or by + # adding a 'data' handler, or by calling the .resume() + # method. Until the data is consumed, the 'end' event will not + # fire. Also, until the data is read it will consume memory + # that can eventually lead to a 'process out of memory' error. + urlStream.on 'data', () -> # discard the data callbackOnce(new Error("URL returned non-success status code: #{res.statusCode} #{url}")) urlStream.on "error", (error) -> From fd6386207b1604873c972b6eba2056cff5d223ff Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 29 Apr 2015 16:04:32 +0100 Subject: [PATCH 041/709] remove debugging from tests --- test/unit/coffee/CompileManagerTests.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 8923dea0..d5644b6b 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -71,7 +71,6 @@ describe "CompileManager", -> .should.equal true it "should return the output files", -> - console.log 'output_files', @build_files @callback.calledWith(null, @build_files).should.equal true describe "clearProject", -> From d9d16b7189a59ef3187b6f4cca22cabf082a8adc Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 30 Apr 2015 15:03:41 +0100 Subject: [PATCH 042/709] make startup message consistent --- app.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 464ff390..42c5c062 100644 --- a/app.coffee +++ b/app.coffee @@ -104,7 +104,7 @@ app.use (error, req, res, next) -> res.send error?.statusCode || 500 app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) -> - logger.log "CLSI listening on #{host}:#{port}" + logger.info "CLSI starting up, listening on #{host}:#{port}" setInterval () -> ProjectPersistenceManager.clearExpiredProjects() From 2e91868bc8b73a0bd6002d91d147802b437935ba Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 5 May 2015 09:47:17 +0100 Subject: [PATCH 043/709] disable sequelize logging by default prevent any leaking of objects to console during debugging --- app/coffee/db.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index 5438fc59..203915ef 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -1,11 +1,14 @@ Sequelize = require("sequelize") Settings = require("settings-sharelatex") +_ = require("underscore") + +options = _.extend {logging:false}, Settings.mysql.clsi sequelize = new Sequelize( Settings.mysql.clsi.database, Settings.mysql.clsi.username, Settings.mysql.clsi.password, - Settings.mysql.clsi + options ) module.exports = From d90fe49a4e9001d348d8496f098f1c8830551cab Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 5 May 2015 09:51:45 +0100 Subject: [PATCH 044/709] avoid leak when calling chai.should() repeatedly in smoke test --- test/smoke/coffee/SmokeTests.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/smoke/coffee/SmokeTests.coffee b/test/smoke/coffee/SmokeTests.coffee index f560b0cf..48c50d66 100644 --- a/test/smoke/coffee/SmokeTests.coffee +++ b/test/smoke/coffee/SmokeTests.coffee @@ -1,5 +1,5 @@ chai = require("chai") -chai.should() +chai.should() unless Object.prototype.should? expect = chai.expect request = require "request" Settings = require "settings-sharelatex" From 58ecfa69e686845e28f3770f805cad8fca4d9cb2 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 5 May 2015 10:54:59 +0100 Subject: [PATCH 045/709] use the latest versions of metrics and smoketest modules --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8bf90356..3d02800b 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "request": "~2.21.0", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git", "sequelize": "2.0.0-beta.2", "wrench": "~1.5.4", - "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v1.0.0", + "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git", "sqlite3": "~2.2.0", "express": "^4.2.0", "body-parser": "^1.2.0", From 9d3fdcf8b4046d7f7b45409f7dc404569610a286 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 11 May 2015 12:10:13 +0100 Subject: [PATCH 046/709] additional validation of requests --- app/coffee/StaticServerForbidSymlinks.coffee | 19 +++++- .../StaticServerForbidSymlinksTests.coffee | 65 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/app/coffee/StaticServerForbidSymlinks.coffee b/app/coffee/StaticServerForbidSymlinks.coffee index 2f481908..348eccab 100644 --- a/app/coffee/StaticServerForbidSymlinks.coffee +++ b/app/coffee/StaticServerForbidSymlinks.coffee @@ -9,7 +9,24 @@ module.exports = ForbidSymlinks = (staticFn, root, options) -> basePath = Path.resolve(root) return (req, res, next) -> path = url.parse(req.url)?.pathname - requestedFsPath = Path.normalize("#{basePath}/#{path}") + # check that the path is of the form /project_id/path/to/file + if result = path.match(/^\/?(\w+)\/(.*)/) + project_id = result[1] + file = result[2] + else + logger.warn path: path, "unrecognized file request" + return res.sendStatus(404) + # check that the file does not use a relative path + for dir in file.split('/') + if dir == '..' + logger.warn path: path, "attempt to use a relative path" + return res.sendStatus(404) + # check that the requested path is normalized + requestedFsPath = "#{basePath}/#{project_id}/#{file}" + if requestedFsPath != Path.normalize(requestedFsPath) + logger.error path: requestedFsPath, "requestedFsPath is not normalized" + return res.sendStatus(404) + # check that the requested path is not a symlink fs.realpath requestedFsPath, (err, realFsPath)-> if err? logger.warn err:err, requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "error checking file access" diff --git a/test/unit/coffee/StaticServerForbidSymlinksTests.coffee b/test/unit/coffee/StaticServerForbidSymlinksTests.coffee index e17bacef..e6b7f5f9 100644 --- a/test/unit/coffee/StaticServerForbidSymlinksTests.coffee +++ b/test/unit/coffee/StaticServerForbidSymlinksTests.coffee @@ -20,6 +20,7 @@ describe "StaticServerForbidSymlinks", -> "logger-sharelatex": log:-> warn:-> + error:-> "fs":@fs @dummyStatic = (rootDir, options) -> @@ -69,6 +70,70 @@ describe "StaticServerForbidSymlinks", -> done() @StaticServerForbidSymlinks @req, @res + + describe "with a relative file", -> + beforeEach -> + @req.url = "/12345/../67890/output.pdf" + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + + + describe "with a unnormalized file containing .", -> + beforeEach -> + @req.url = "/12345/foo/./output.pdf" + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + + + describe "with a file containing an empty path", -> + beforeEach -> + @req.url = "/12345/foo//output.pdf" + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + + describe "with a non-project file", -> + beforeEach -> + @req.url = "/.foo/output.pdf" + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + + describe "with a file outside the compiledir", -> + beforeEach -> + @req.url = "/../bar/output.pdf" + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + + + describe "with a file with no leading /", -> + beforeEach -> + @req.url = "./../bar/output.pdf" + + it "should send a 404", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 404 + done() + @StaticServerForbidSymlinks @req, @res + describe "with an error from fs.realpath", -> beforeEach -> From 94c4187eb2ce68fe6768f83f51fb144225540035 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 12 May 2015 15:17:18 +0100 Subject: [PATCH 047/709] change regex checking file request ensure other files can not be accessed --- app/coffee/StaticServerForbidSymlinks.coffee | 4 ++-- .../coffee/StaticServerForbidSymlinksTests.coffee | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/coffee/StaticServerForbidSymlinks.coffee b/app/coffee/StaticServerForbidSymlinks.coffee index 348eccab..83ca4ca7 100644 --- a/app/coffee/StaticServerForbidSymlinks.coffee +++ b/app/coffee/StaticServerForbidSymlinks.coffee @@ -9,8 +9,8 @@ module.exports = ForbidSymlinks = (staticFn, root, options) -> basePath = Path.resolve(root) return (req, res, next) -> path = url.parse(req.url)?.pathname - # check that the path is of the form /project_id/path/to/file - if result = path.match(/^\/?(\w+)\/(.*)/) + # check that the path is of the form /project_id_or_name/path/to/file.log + if result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/) project_id = result[1] file = result[2] else diff --git a/test/unit/coffee/StaticServerForbidSymlinksTests.coffee b/test/unit/coffee/StaticServerForbidSymlinksTests.coffee index e6b7f5f9..4a87d642 100644 --- a/test/unit/coffee/StaticServerForbidSymlinksTests.coffee +++ b/test/unit/coffee/StaticServerForbidSymlinksTests.coffee @@ -134,6 +134,17 @@ describe "StaticServerForbidSymlinks", -> done() @StaticServerForbidSymlinks @req, @res + describe "with a github style path", -> + beforeEach -> + @req.url = "/henryoswald-latex_example/output/output.log" + @fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/henryoswald-latex_example/output/output.log") + + it "should call next", (done)-> + @res.sendStatus = (resCode)-> + resCode.should.equal 200 + done() + @StaticServerForbidSymlinks @req, @res, done + describe "with an error from fs.realpath", -> beforeEach -> From 558b5e000fef4ba81227a7dc1f9b0f1a5e0ff10d Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 13 May 2015 16:59:51 +0100 Subject: [PATCH 048/709] only run qpdf for the main output.pdf file was previously matching any pdf file, which caused it to run for embedded pdf figures produced during the mklatex run --- app/coffee/OutputFileOptimiser.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputFileOptimiser.coffee b/app/coffee/OutputFileOptimiser.coffee index d0c091d3..f337a7a9 100644 --- a/app/coffee/OutputFileOptimiser.coffee +++ b/app/coffee/OutputFileOptimiser.coffee @@ -9,7 +9,7 @@ module.exports = OutputFileOptimiser = optimiseFile: (src, dst, callback = (error) ->) -> # check output file (src) and see if we can optimise it, storing # the result in the build directory (dst) - if src.match(/\.pdf$/) + if src.match(/\/output\.pdf$/) OutputFileOptimiser.optimisePDF src, dst, callback else callback (null) From 889fa65d0c5093d85669c11fe180bf5795bf07f9 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 15 May 2015 14:17:40 +0100 Subject: [PATCH 049/709] clean up stream handling for file copy --- app/coffee/UrlCache.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index 941bc9de..be6960c2 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -75,8 +75,9 @@ module.exports = UrlCache = readStream = fs.createReadStream(from) writeStream.on "error", callbackOnce readStream.on "error", callbackOnce - writeStream.on "close", () -> callbackOnce() - readStream.pipe(writeStream) + writeStream.on "close", callbackOnce + writeStream.on "open", () -> + readStream.pipe(writeStream) _clearUrlFromCache: (project_id, url, callback = (error) ->) -> UrlCache._clearUrlDetails project_id, url, (error) -> From 03c105c3d99dfa803e6f8476042799d01d535ce3 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 12 May 2015 11:40:29 +0100 Subject: [PATCH 050/709] replace deprecated send(code,body) calls --- app.coffee | 4 ++-- app/coffee/CompileController.coffee | 4 ++-- test/unit/coffee/CompileControllerTests.coffee | 13 +++++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app.coffee b/app.coffee index 42c5c062..9083bb4b 100644 --- a/app.coffee +++ b/app.coffee @@ -84,7 +84,7 @@ if Settings.smokeTest app.get "/health_check", (req, res)-> res.contentType(resCacher?.setContentType) - res.send resCacher?.code, resCacher?.body + res.status(resCacher?.code).send(resCacher?.body) profiler = require "v8-profiler" app.get "/profile", (req, res) -> @@ -101,7 +101,7 @@ app.get "/heapdump", (req, res)-> app.use (error, req, res, next) -> logger.error err: error, "server error" - res.send error?.statusCode || 500 + res.sendStatus(error?.statusCode || 500) app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) -> logger.info "CLSI starting up, listening on #{host}:#{port}" diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 29373c36..71368be7 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -28,7 +28,7 @@ module.exports = CompileController = status = "success" timer.done() - res.send (code or 200), { + res.status(code or 200).send { compile: status: status error: error?.message or error @@ -41,7 +41,7 @@ module.exports = CompileController = clearCache: (req, res, next = (error) ->) -> ProjectPersistenceManager.clearProject req.params.project_id, (error) -> return next(error) if error? - res.send 204 # No content + res.sendStatus(204) # No content syncFromCode: (req, res, next = (error) ->) -> file = req.query.file diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index bc2d9881..3298f472 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -44,6 +44,7 @@ describe "CompileController", -> }] @RequestParser.parse = sinon.stub().callsArgWith(1, null, @request) @ProjectPersistenceManager.markProjectAsJustAccessed = sinon.stub().callsArg(1) + @res.status = sinon.stub().returnsThis() @res.send = sinon.stub() describe "successfully", -> @@ -67,8 +68,9 @@ describe "CompileController", -> .should.equal true it "should return the JSON response", -> + @res.status.calledWith(200).should.equal true @res.send - .calledWith(200, + .calledWith( compile: status: "success" error: null @@ -85,8 +87,9 @@ describe "CompileController", -> @CompileController.compile @req, @res it "should return the JSON response with the error", -> + @res.status.calledWith(500).should.equal true @res.send - .calledWith(500, + .calledWith( compile: status: "error" error: @message @@ -102,8 +105,9 @@ describe "CompileController", -> @CompileController.compile @req, @res it "should return the JSON response with the timeout status", -> + @res.status.calledWith(200).should.equal true @res.send - .calledWith(200, + .calledWith( compile: status: "timedout" error: @message @@ -117,8 +121,9 @@ describe "CompileController", -> @CompileController.compile @req, @res it "should return the JSON response with the failure status", -> + @res.status.calledWith(200).should.equal true @res.send - .calledWith(200, + .calledWith( compile: error: null status: "failure" From 3c97b98fc407cb224324ea5a80631454e82fb669 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 15 May 2015 14:07:15 +0100 Subject: [PATCH 051/709] clean up error handling in UrlFetcher --- app/coffee/UrlFetcher.coffee | 70 ++++++++++++++----------- test/unit/coffee/UrlFetcherTests.coffee | 16 +++--- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.coffee index e14285d5..9675da9b 100644 --- a/app/coffee/UrlFetcher.coffee +++ b/app/coffee/UrlFetcher.coffee @@ -2,33 +2,53 @@ request = require("request").defaults(jar: false) fs = require("fs") logger = require "logger-sharelatex" +oneMinute = 60 * 1000 + module.exports = UrlFetcher = pipeUrlToFile: (url, filePath, _callback = (error) ->) -> callbackOnce = (error) -> - cleanUp error, (error) -> - _callback(error) - _callback = () -> - - cleanUp = (error, callback) -> - if error? - logger.log filePath: filePath, "deleting file from cache due to error" - fs.unlink filePath, (err) -> - if err? - logger.err err: err, filePath: filePath, "error deleting file from cache" - callback(error) - else - callback() + clearTimeout timeoutHandler if timeoutHandler? + _callback(error) + _callback = () -> + + timeoutHandler = setTimeout () -> + timeoutHandler = null + logger.error url:url, filePath: filePath, "Timed out downloading file to cache" + callbackOnce(new Error("Timed out downloading file to cache #{url}")) + # FIXME: maybe need to close fileStream here + , 3 * oneMinute + + logger.log url:url, filePath: filePath, "started downloading url to cache" + urlStream = request.get({url: url, timeout: oneMinute}) + urlStream.pause() + + urlStream.on "error", (error) -> + logger.error err: error, url:url, filePath: filePath, "error downloading url" + callbackOnce(error or new Error("Something went wrong downloading the URL #{url}")) - fileStream = fs.createWriteStream(filePath) - fileStream.on 'error', (error) -> - logger.error err: error, url:url, filePath: filePath, "error writing file into cache" - callbackOnce(error) + urlStream.on "end", () -> + logger.log url:url, filePath: filePath, "finished downloading file into cache" - logger.log url:url, filePath: filePath, "downloading url to cache" - urlStream = request.get(url) urlStream.on "response", (res) -> if res.statusCode >= 200 and res.statusCode < 300 + fileStream = fs.createWriteStream(filePath) + + fileStream.on 'error', (error) -> + logger.error err: error, url:url, filePath: filePath, "error writing file into cache" + fs.unlink filePath, (err) -> + if err? + logger.err err: err, filePath: filePath, "error deleting file from cache" + callbackOnce(error) + + fileStream.on 'finish', () -> + logger.log url:url, filePath: filePath, "finished writing file into cache" + callbackOnce() + + fileStream.on 'pipe', () -> + logger.log url:url, filePath: filePath, "piping into filestream" + urlStream.pipe(fileStream) + urlStream.resume() else logger.error statusCode: res.statusCode, url:url, filePath: filePath, "unexpected status code downloading url to cache" # https://nodejs.org/api/http.html#http_class_http_clientrequest @@ -39,15 +59,5 @@ module.exports = UrlFetcher = # method. Until the data is consumed, the 'end' event will not # fire. Also, until the data is read it will consume memory # that can eventually lead to a 'process out of memory' error. - urlStream.on 'data', () -> # discard the data + urlStream.resume() # discard the data callbackOnce(new Error("URL returned non-success status code: #{res.statusCode} #{url}")) - - urlStream.on "error", (error) -> - logger.error err: error, url:url, filePath: filePath, "error downloading url" - callbackOnce(error or new Error("Something went wrong downloading the URL #{url}")) - - urlStream.on "end", () -> - # FIXME: what if we get an error writing the file? Maybe we - # should be using the fileStream end event as the point of - # callback. - callbackOnce() diff --git a/test/unit/coffee/UrlFetcherTests.coffee b/test/unit/coffee/UrlFetcherTests.coffee index 7d38aa66..dd709dde 100644 --- a/test/unit/coffee/UrlFetcherTests.coffee +++ b/test/unit/coffee/UrlFetcherTests.coffee @@ -22,25 +22,29 @@ describe "UrlFetcher", -> @path = "/path/to/file/on/disk" @request.get = sinon.stub().returns(@urlStream = new EventEmitter) @urlStream.pipe = sinon.stub() - @fs.createWriteStream = sinon.stub().returns(@fileStream = { on: () -> }) + @urlStream.pause = sinon.stub() + @urlStream.resume = sinon.stub() + @fs.createWriteStream = sinon.stub().returns(@fileStream = new EventEmitter) @fs.unlink = (file, callback) -> callback() @UrlFetcher.pipeUrlToFile(@url, @path, @callback) it "should request the URL", -> @request.get - .calledWith(@url) + .calledWith(sinon.match {"url": @url}) .should.equal true - it "should open the file for writing", -> - @fs.createWriteStream - .calledWith(@path) - .should.equal true describe "successfully", -> beforeEach -> @res = statusCode: 200 @urlStream.emit "response", @res @urlStream.emit "end" + @fileStream.emit "finish" + + it "should open the file for writing", -> + @fs.createWriteStream + .calledWith(@path) + .should.equal true it "should pipe the URL to the file", -> @urlStream.pipe From 484c0da84fc27e55f27a50f027438c662893a222 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 15 May 2015 14:05:17 +0100 Subject: [PATCH 052/709] add indexes to db --- app/coffee/db.coffee | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index 203915ef..a72f61e8 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -16,11 +16,20 @@ module.exports = url: Sequelize.STRING project_id: Sequelize.STRING lastModified: Sequelize.DATE + }, { + indexes: [ + {fields: ['url', 'project_id']}, + {fields: ['project_id']} + ] }) Project: sequelize.define("Project", { - project_id: Sequelize.STRING + project_id: {type: Sequelize.STRING, primaryKey: true} lastAccessed: Sequelize.DATE + }, { + indexes: [ + {fields: ['lastAccessed']} + ] }) sync: () -> sequelize.sync() From 7d8bc9fed9f5fc53a5afba7cf133a362f436c4b6 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 15 May 2015 14:05:28 +0100 Subject: [PATCH 053/709] upgrade sequelize and mysql --- app/coffee/ProjectPersistenceManager.coffee | 20 +++++++++----------- app/coffee/UrlCache.coffee | 16 ++++++++-------- package.json | 4 ++-- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 8b379473..2e23d46c 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -8,11 +8,11 @@ module.exports = ProjectPersistenceManager = EXPIRY_TIMEOUT: oneDay = 24 * 60 * 60 * 1000 #ms markProjectAsJustAccessed: (project_id, callback = (error) ->) -> - db.Project.findOrCreate(project_id: project_id) - .success( - (project) -> + db.Project.findOrCreate(where: {project_id: project_id}) + .spread( + (project, created) -> project.updateAttributes(lastAccessed: new Date()) - .success(() -> callback()) + .then(() -> callback()) .error callback ) .error callback @@ -41,14 +41,12 @@ module.exports = ProjectPersistenceManager = callback() _clearProjectFromDatabase: (project_id, callback = (error) ->) -> - db.Project.destroy(project_id: project_id) - .success(() -> callback()) + db.Project.destroy(where: {project_id: project_id}) + .then(() -> callback()) .error callback _findExpiredProjectIds: (callback = (error, project_ids) ->) -> db.Project.findAll(where: ["lastAccessed < ?", new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT)]) - .success( - (projects) -> - callback null, projects.map((project) -> project.project_id) - ) - .error callback + .then((projects) -> + callback null, projects.map((project) -> project.project_id) + ).error callback diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index be6960c2..535a7057 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -91,27 +91,27 @@ module.exports = UrlCache = _findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) -> db.UrlCache.find(where: { url: url, project_id: project_id }) - .success((urlDetails) -> callback null, urlDetails) + .then((urlDetails) -> callback null, urlDetails) .error callback _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) -> - db.UrlCache.findOrCreate(url: url, project_id: project_id) - .success( - (urlDetails) -> + db.UrlCache.findOrCreate(where: {url: url, project_id: project_id}) + .spread( + (urlDetails, created) -> urlDetails.updateAttributes(lastModified: lastModified) - .success(() -> callback()) + .then(() -> callback()) .error(callback) ) .error callback _clearUrlDetails: (project_id, url, callback = (error) ->) -> - db.UrlCache.destroy(url: url, project_id: project_id) - .success(() -> callback null) + db.UrlCache.destroy(where: {url: url, project_id: project_id}) + .then(() -> callback null) .error callback _findAllUrlsInProject: (project_id, callback = (error, urls) ->) -> db.UrlCache.findAll(where: { project_id: project_id }) - .success( + .then( (urlEntries) -> callback null, urlEntries.map((entry) -> entry.url) ) diff --git a/package.json b/package.json index 3d02800b..fc75cede 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ "async": "0.2.9", "lynx": "0.0.11", "mkdirp": "0.3.5", - "mysql": "2.0.0-alpha7", + "mysql": "2.6.2", "request": "~2.21.0", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git", - "sequelize": "2.0.0-beta.2", + "sequelize": "^2.1.3", "wrench": "~1.5.4", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git", "sqlite3": "~2.2.0", From ff5b203ecf8ef4b33626e16aecfad8cf897af9d2 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 21 May 2015 11:33:13 +0100 Subject: [PATCH 054/709] added comments --- app/coffee/UrlFetcher.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.coffee index 9675da9b..201306c0 100644 --- a/app/coffee/UrlFetcher.coffee +++ b/app/coffee/UrlFetcher.coffee @@ -20,8 +20,9 @@ module.exports = UrlFetcher = logger.log url:url, filePath: filePath, "started downloading url to cache" urlStream = request.get({url: url, timeout: oneMinute}) - urlStream.pause() + urlStream.pause() # stop data flowing until we are ready + # attach handlers before setting up pipes urlStream.on "error", (error) -> logger.error err: error, url:url, filePath: filePath, "error downloading url" callbackOnce(error or new Error("Something went wrong downloading the URL #{url}")) @@ -33,6 +34,7 @@ module.exports = UrlFetcher = if res.statusCode >= 200 and res.statusCode < 300 fileStream = fs.createWriteStream(filePath) + # attach handlers before setting up pipes fileStream.on 'error', (error) -> logger.error err: error, url:url, filePath: filePath, "error writing file into cache" fs.unlink filePath, (err) -> @@ -48,7 +50,7 @@ module.exports = UrlFetcher = logger.log url:url, filePath: filePath, "piping into filestream" urlStream.pipe(fileStream) - urlStream.resume() + urlStream.resume() # now we are ready to handle the data else logger.error statusCode: res.statusCode, url:url, filePath: filePath, "unexpected status code downloading url to cache" # https://nodejs.org/api/http.html#http_class_http_clientrequest From 25d73ab6f9db30b7c35b4e1936697783484172a4 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 8 Jun 2015 18:35:24 -0300 Subject: [PATCH 055/709] initial version of texcount --- app.coffee | 1 + app/coffee/CompileController.coffee | 10 ++++ app/coffee/CompileManager.coffee | 50 +++++++++++++++++++- test/acceptance/coffee/SynctexTests.coffee | 17 +++++++ test/acceptance/coffee/helpers/Client.coffee | 9 ++++ 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 9083bb4b..8e451a76 100644 --- a/app.coffee +++ b/app.coffee @@ -36,6 +36,7 @@ app.delete "/project/:project_id", CompileController.clearCache app.get "/project/:project_id/sync/code", CompileController.syncFromCode app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf +app.get "/project/:project_id/wordcount", CompileController.wordcount ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks" diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 71368be7..ce107f8e 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -66,3 +66,13 @@ module.exports = CompileController = res.send JSON.stringify { code: codePositions } + + wordcount: (req, res, next = (error) ->) -> + file = req.query.file + project_id = req.params.project_id + + CompileManager.wordcount project_id, file, (error, result) -> + return next(error) if error? + res.send JSON.stringify { + texcount: result + } diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index ac0c2411..5bbd3ab2 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -107,4 +107,52 @@ module.exports = CompileManager = line: parseInt(line, 10) column: parseInt(column, 10) } - return results \ No newline at end of file + return results + + _parseWordcountFromOutput: (output) -> + results = { + encode: "" + textWords: 0 + headWords: 0 + outside: 0 + headers: 0 + elements: 0 + mathInline: 0 + mathDisplay: 0 + } + for line in output.split("\n") + [data, info] = line.split(":") + if data.indexOf("Encoding") > -1 + results['encode'] = info.trim() + if data.indexOf("in text") > -1 + results['textWords'] = parseInt(info, 10) + if data.indexOf("in head") > -1 + results['headWords'] = parseInt(info, 10) + if data.indexOf("outside") > -1 + results['outside'] = parseInt(info, 10) + if data.indexOf("of head") > -1 + results['headers'] = parseInt(info, 10) + if data.indexOf("float") > -1 + results['elements'] = parseInt(info, 10) + if data.indexOf("inlines") > -1 + results['mathInline'] = parseInt(info, 10) + if data.indexOf("displayed") > -1 + results['mathDisplay'] = parseInt(info, 10) + + return results + + wordcount: (project_id, file_name, callback = (error, pdfPositions) ->) -> + base_dir = Settings.path.synctexBaseDir(project_id) + file_path = base_dir + "/" + file_name + logger.log project_id: project_id, file_name: file_name, "try wordcount" + CompileManager._runWordcount [file_path], (error, stdout) -> + return callback(error) if error? + logger.log project_id: project_id, file_name: file_name, stdout: stdout, "wordcount output" + callback null, CompileManager._parseWordcountFromOutput(stdout) + + _runWordcount: (args, callback = (error, stdout) ->) -> + bin_path = Path.resolve("texcount") + seconds = 1000 + child_process.execFile "texcount", args, timeout: 10 * seconds, (error, stdout, stderr) -> + return callback(error) if error? + callback(null, stdout) \ No newline at end of file diff --git a/test/acceptance/coffee/SynctexTests.coffee b/test/acceptance/coffee/SynctexTests.coffee index 330eaff2..8979b983 100644 --- a/test/acceptance/coffee/SynctexTests.coffee +++ b/test/acceptance/coffee/SynctexTests.coffee @@ -36,3 +36,20 @@ describe "Syncing", -> ) done() + describe "wordcount file", -> + it "should return wordcount info", (done) -> + Client.wordcount @project_id, "main.tex", (error, result) -> + throw error if error? + expect(result).to.deep.equal( + texcount: { + encode: "ascii" + textWords: 2 + headWords: 0 + outside: 0 + headers: 0 + elements: 0 + mathInline: 0 + mathDisplay: 0 + } + ) + done() diff --git a/test/acceptance/coffee/helpers/Client.coffee b/test/acceptance/coffee/helpers/Client.coffee index 7025b79c..3e1a5b86 100644 --- a/test/acceptance/coffee/helpers/Client.coffee +++ b/test/acceptance/coffee/helpers/Client.coffee @@ -90,3 +90,12 @@ module.exports = Client = @compile project_id, req, callback + wordcount: (project_id, file, callback = (error, pdfPositions) ->) -> + request.get { + url: "#{@host}/project/#{project_id}/wordcount" + qs: { + file: file + } + }, (error, response, body) -> + return callback(error) if error? + callback null, JSON.parse(body) From a04d951b405c13b174d0ec5798db491bf4363b86 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 8 Jun 2015 19:27:47 -0300 Subject: [PATCH 056/709] add unit test --- app/coffee/CompileManager.coffee | 2 -- .../unit/coffee/CompileControllerTests.coffee | 24 ++++++++++++++ test/unit/coffee/CompileManagerTests.coffee | 31 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 5bbd3ab2..f689ae88 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -144,14 +144,12 @@ module.exports = CompileManager = wordcount: (project_id, file_name, callback = (error, pdfPositions) ->) -> base_dir = Settings.path.synctexBaseDir(project_id) file_path = base_dir + "/" + file_name - logger.log project_id: project_id, file_name: file_name, "try wordcount" CompileManager._runWordcount [file_path], (error, stdout) -> return callback(error) if error? logger.log project_id: project_id, file_name: file_name, stdout: stdout, "wordcount output" callback null, CompileManager._parseWordcountFromOutput(stdout) _runWordcount: (args, callback = (error, stdout) ->) -> - bin_path = Path.resolve("texcount") seconds = 1000 child_process.execFile "texcount", args, timeout: 10 * seconds, (error, stdout, stderr) -> return callback(error) if error? diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index 3298f472..e0307d20 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -189,3 +189,27 @@ describe "CompileController", -> ) .should.equal true + describe "wordcount", -> + beforeEach -> + @file = "main.tex" + @project_id = "mock-project-id" + @req.params = + project_id: @project_id + @req.query = + file: @file + @res.send = sinon.stub() + + @CompileManager.wordcount = sinon.stub().callsArgWith(2, null, @texcount = ["mock-texcount"]) + @CompileController.wordcount @req, @res, @next + + it "should return the word count of a file", -> + @CompileManager.wordcount + .calledWith(@project_id, @file) + .should.equal true + + it "should return the texcount info", -> + @res.send + .calledWith(JSON.stringify + texcount: @texcount + ) + .should.equal true diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index d5644b6b..eae49627 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -171,4 +171,33 @@ describe "CompileManager", -> line: @line column: @column }]) - .should.equal true \ No newline at end of file + .should.equal true + + describe "wordcount", -> + beforeEach -> + @file_name = "main.tex" + @child_process.execFile = sinon.stub() + @child_process.execFile.callsArgWith(3, null, @stdout = "Encoding: ascii\nWords in text: 2", "") + @Settings.path.synctexBaseDir = (project_id) => "#{@Settings.path.compilesDir}/#{@project_id}" + @CompileManager.wordcount @project_id, @file_name, @callback + + it "should execute the texcount binary", -> + file_path = "#{@Settings.path.compilesDir}/#{@project_id}/#{@file_name}" + @child_process.execFile + .calledWith("texcount", [file_path], timeout: 10000) + .should.equal true + + it "should call the callback with the parsed output", -> + @callback + .calledWith(null, { + encode: "ascii" + textWords: 2 + headWords: 0 + outside: 0 + headers: 0 + elements: 0 + mathInline: 0 + mathDisplay: 0 + }) + .should.equal true + From 71d2427cf173d4096e044e7a264bc0014ced12a3 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 12 Jun 2015 17:11:03 +0100 Subject: [PATCH 057/709] added some load tests in --- test/load/coffee/bulk.tex | 234 +++++++++++++++++++++++++++++++ test/load/coffee/loadTest.coffee | 71 ++++++++++ 2 files changed, 305 insertions(+) create mode 100644 test/load/coffee/bulk.tex create mode 100644 test/load/coffee/loadTest.coffee diff --git a/test/load/coffee/bulk.tex b/test/load/coffee/bulk.tex new file mode 100644 index 00000000..67c4772c --- /dev/null +++ b/test/load/coffee/bulk.tex @@ -0,0 +1,234 @@ + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tincidunt mattis sapien at tempor. Mauris ac tristique erat. Praesent interdum ipsum sem, ac fermentum urna imperdiet in. Nulla tincidunt purus vitae ipsum sagittis tincidunt. Aenean id nisi ullamcorper, ultrices mi vel, iaculis nunc. Sed vel varius metus, ac eleifend mauris. Donec sed orci fringilla, fermentum nulla vehicula, sodales purus. + +Maecenas nulla quam, congue vitae pellentesque sed, bibendum eu felis. Vestibulum congue gravida diam, in venenatis nisl lacinia id. Nullam eget purus ac enim dignissim consectetur vel at dolor. Integer rhoncus nisl eu odio luctus, at placerat dolor congue. Fusce sodales molestie sem eget scelerisque. Sed eros tellus, tempor eu commodo nec, maximus imperdiet eros. Aliquam vulputate ligula non bibendum tempus. In commodo eros ante, ultrices condimentum purus finibus ut. Suspendisse at eleifend mauris, vitae tincidunt sapien. Curabitur orci ipsum, aliquet a cursus efficitur, lacinia ac ex. Integer lacinia bibendum dui ut ullamcorper. Curabitur in ultricies tellus, quis ullamcorper sem. Praesent sodales dui odio. Ut lacinia aliquet eros, ut maximus nisi. Donec sit amet dui a neque interdum dapibus. + +Ut vulputate sem in lectus porttitor ullamcorper. Nulla ut urna vitae tellus posuere aliquam vitae in odio. Praesent placerat laoreet viverra. Curabitur lacinia est lectus, eget euismod nisi viverra eget. Aliquam facilisis lectus ut tincidunt mollis. Donec ut rhoncus lorem. Vivamus ultricies venenatis congue. Etiam non risus quis leo sodales lacinia. Phasellus commodo feugiat sem quis dignissim. Nunc augue dui, bibendum sed leo vitae, malesuada vulputate sem. + +Quisque nec semper nulla. Etiam dictum blandit interdum. Morbi leo leo, scelerisque vel enim vel, egestas volutpat ligula. Maecenas ac elementum lacus. Duis molestie nunc id metus iaculis, in hendrerit massa egestas. Praesent feugiat tempor dui, sit amet ultrices dui elementum id. Suspendisse cursus accumsan diam, non imperdiet diam dapibus facilisis. Praesent blandit urna felis, eget sodales nisi dictum non. Cras finibus quis augue a venenatis. In pretium condimentum arcu, at vehicula ex gravida ut. Etiam congue urna ipsum, mattis interdum neque cursus bibendum. + +Morbi felis orci, ultricies eget magna gravida, blandit condimentum erat. Curabitur convallis quam eros, eu porta diam ornare vitae. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eleifend convallis massa, eget tristique dolor iaculis sed. Mauris id nunc erat. Donec semper rhoncus libero sit amet rhoncus. Suspendisse cursus suscipit augue quis fermentum. Sed in maximus erat. + +Ut ultrices massa vitae lectus dictum fermentum. Cras vitae risus metus. Curabitur eleifend hendrerit dolor sit amet rutrum. Pellentesque pellentesque dolor ut felis vehicula pharetra. Nam id ante eget turpis vehicula interdum in vitae odio. Nullam nec orci interdum, commodo massa et, rutrum purus. Aenean vitae porta sem. Nam in lacinia turpis. Duis dui ligula, molestie quis sagittis sit amet, faucibus ac leo. Curabitur sit amet porta ligula. Integer et sollicitudin velit. Donec magna justo, ultricies eu nunc ut, rutrum aliquam orci. Sed in dignissim sem. Proin rutrum velit urna, eu tincidunt ipsum fermentum non. Morbi id cursus nisl. + +Curabitur sed gravida ex, posuere laoreet orci. Morbi ac lacus quis tortor faucibus feugiat. Etiam fringilla lacinia libero. Duis varius sem vel lorem euismod luctus. Fusce tincidunt quis sem in ullamcorper. Ut luctus massa aliquam hendrerit finibus. Ut venenatis, neque eu hendrerit finibus, nisl tortor venenatis eros, in imperdiet leo est quis erat. Fusce luctus posuere massa, ut fermentum sapien blandit ut. Maecenas feugiat consequat lorem, eget sagittis elit vestibulum sit amet. Vivamus molestie ante ut turpis laoreet facilisis vitae eu diam. Integer a tempor tortor. In hac habitasse platea dictumst. Quisque arcu est, blandit eu justo sed, posuere congue nisi. Aliquam magna augue, convallis ac scelerisque vel, cursus eget dui. Nam rutrum auctor odio, vel sagittis ipsum gravida vel. + +Etiam elementum placerat egestas. Morbi nec mi posuere, congue ligula eu, sagittis turpis. Fusce urna nisi, dapibus in pretium et, lobortis eu arcu. Curabitur ornare urna mauris, vitae varius nulla posuere in. Integer faucibus euismod dui, a venenatis massa vehicula sit amet. Donec fringilla tellus vitae ligula pretium mattis. Aliquam aliquet quam augue, a luctus orci euismod sed. Morbi tincidunt tincidunt nulla, eget elementum turpis congue id. Suspendisse pellentesque nulla leo, fermentum ultrices massa sollicitudin vel. Morbi vel nisl consectetur, pulvinar sapien a, accumsan diam. Morbi posuere auctor nibh, nec maximus ante tincidunt ac. Etiam ut erat consectetur, molestie est sit amet, pharetra nulla. Quisque varius vestibulum ex, eget feugiat enim molestie ac. Nulla quis imperdiet risus. + +Nullam nec tempor arcu. Duis fringilla mi at magna dignissim, quis feugiat turpis lacinia. Nunc sed aliquet ipsum. Curabitur at dolor in dui posuere ornare a ut ex. Ut congue neque quis justo iaculis, ut accumsan odio condimentum. Donec sed tempus diam. Phasellus tincidunt malesuada dui, nec gravida justo volutpat vel. Praesent mi purus, sagittis in imperdiet sed, sodales eu turpis. Nullam rutrum non lacus ac imperdiet. Ut ultrices lorem at facilisis feugiat. Morbi eros enim, tristique at nisl ut, venenatis porttitor ligula. Nullam sed diam at nibh tristique consectetur. Phasellus iaculis justo nisi, ut interdum ante rutrum sit amet. Pellentesque finibus felis blandit metus pulvinar lacinia. + +Aliquam erat volutpat. Nulla eu tortor sit amet tellus bibendum tristique eget consequat metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut in aliquet augue. Phasellus laoreet nulla convallis finibus vehicula. Fusce et urna dui. Duis vel porta nunc. Nunc condimentum, justo at efficitur dignissim, lorem diam elementum ex, at dictum lectus sapien ac neque. Aliquam lacinia et ipsum lacinia efficitur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus convallis urna orci, et dictum sapien porta sit amet. Maecenas euismod dolor mattis sapien vestibulum pulvinar. + +Vestibulum eget posuere purus, et viverra est. Nullam egestas massa et finibus semper. Vestibulum egestas porta ante eget maximus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque bibendum magna et fermentum consequat. Duis non arcu quis justo dignissim tempus at id diam. Praesent condimentum vel leo ac efficitur. Phasellus sollicitudin ipsum ut consectetur euismod. Proin diam eros, placerat sed dui ac, porttitor pellentesque nibh. Curabitur fermentum volutpat enim, in ullamcorper ipsum euismod et. Nunc a justo tortor. Phasellus libero nunc, consectetur ut dolor non, volutpat condimentum metus. + +Ut tincidunt est sem, eu venenatis lectus pretium pretium. Vivamus venenatis, erat nec sollicitudin semper, justo nulla euismod dui, quis tempor libero lectus sit amet neque. Sed in iaculis ipsum. Quisque ultricies sed mi a consequat. Sed tincidunt ante ut turpis vehicula, sed fringilla ligula efficitur. Cras eget suscipit sapien. Ut sed malesuada est, ut tempor leo. Mauris dignissim turpis quis turpis placerat cursus. Vivamus dictum dui sed blandit aliquet. + +Ut cursus, nulla eget ultricies tempor, magna enim aliquam libero, eget tempus mauris mauris ut elit. Nulla a mi quam. Integer ullamcorper ex et enim ornare efficitur. Vivamus tellus orci, pharetra in suscipit ac, ultrices sit amet sapien. Pellentesque pretium mauris vel orci accumsan, a hendrerit lectus sagittis. Mauris id nisi commodo, eleifend arcu in, vestibulum metus. Fusce vulputate gravida tincidunt. Nulla cursus non tortor ut tincidunt. Phasellus vel nisi tempus, fringilla lectus sed, ultricies erat. Ut gravida, enim id facilisis consequat, est nisi scelerisque magna, eget pharetra elit mi elementum ligula. Morbi hendrerit tortor eget velit rhoncus, consequat porta nisl aliquet. Nam diam turpis, ullamcorper vitae nisi eu, ultrices hendrerit magna. Vivamus eget pretium elit. Vivamus vitae odio sit amet libero hendrerit imperdiet. + +Aenean pharetra ex eget lectus sodales placerat. Fusce quis orci vel est suscipit venenatis. Curabitur maximus, sem in tincidunt imperdiet, nisl lorem venenatis mauris, eget facilisis lectus mauris a eros. Nam luctus sem ac diam ultrices, eget vulputate tortor efficitur. Nunc fermentum condimentum lacus id faucibus. Nunc ut tellus pretium, mattis eros vitae, scelerisque felis. Aenean ligula nulla, vulputate id eros id, vestibulum vulputate odio. Nunc in elit id augue porttitor auctor sed vitae lacus. Integer enim orci, auctor at magna eget, viverra tempus risus. Nulla suscipit metus tortor, ultricies vestibulum odio euismod at. Etiam consequat diam ac leo dignissim vulputate. Donec lectus lorem, finibus sed purus ac, eleifend condimentum ipsum. + +Fusce ornare metus vel dui scelerisque vehicula. Proin dictum sapien nec auctor congue. Nunc id erat sed velit facilisis tincidunt. In convallis eu diam id aliquam. Suspendisse eu nisl ante. Sed sit amet arcu non erat sagittis vehicula. Quisque pellentesque at lectus quis maximus. Nam mollis nulla interdum lobortis egestas. Fusce eu tellus eget libero pretium venenatis quis tristique justo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Proin tempor suscipit enim, eget lacinia augue malesuada sit amet. Ut ornare massa in magna pulvinar sagittis. Etiam non risus mi. Aenean aliquam dui et risus egestas aliquet. + +Aenean semper dui risus. Aenean consequat id elit a finibus. Sed vitae est sed arcu interdum maximus interdum in leo. Donec justo lorem, dictum sed placerat sit amet, eleifend in justo. Integer efficitur metus id interdum fringilla. Morbi et dui vitae libero consectetur fermentum quis sed quam. Sed interdum aliquam lorem, at blandit lectus fermentum a. Aliquam ac mollis felis, ut vulputate massa. Praesent convallis cursus eleifend. Donec non sem auctor, efficitur nisi ac, egestas libero. Nullam turpis lacus, dignissim eget pellentesque sed, fermentum ut ipsum. Vestibulum a posuere lacus, vitae rutrum neque. In hac habitasse platea dictumst. Sed vel maximus sem. Etiam dapibus risus et consectetur auctor. Phasellus vestibulum posuere sagittis. + +Aliquam nec libero at velit rhoncus pretium. Curabitur tristique blandit orci id vestibulum. Praesent in tempus arcu. Vivamus in felis tellus. Nunc ac fermentum massa. Cras nisi mi, sollicitudin eu maximus vitae, sodales gravida lorem. Vivamus mollis metus id lectus rhoncus consequat. In dui tellus, vulputate sit amet purus vel, volutpat ornare turpis. Fusce vitae massa non ligula lobortis rhoncus eget id sapien. Sed nec tempus lectus. Proin tempor risus ipsum, fermentum suscipit felis cursus sit amet. + +Maecenas ut dignissim ante, vitae ornare lorem. Fusce nec convallis eros, sed finibus urna. Proin ut finibus dolor. In non nunc sed dui aliquam suscipit. Etiam semper varius ex, sed venenatis sem gravida in. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus tempus aliquet placerat. Nam odio mauris, pharetra ac felis vel, ornare cursus nisl. Phasellus elit risus, finibus id ornare ut, scelerisque sed nisi. Curabitur aliquet, magna in finibus congue, dui libero auctor dui, ut fermentum metus enim vitae ex. Duis at elementum tellus. + +Suspendisse laoreet luctus sem sit amet tempor. Vestibulum non lorem fringilla, maximus nisl vel, pulvinar enim. Suspendisse egestas elit et sem sagittis rhoncus. Morbi nulla augue, semper euismod ultricies quis, maximus et lorem. Nulla nec posuere justo. Ut blandit nisl vitae turpis varius finibus. Donec porttitor eros neque, id mollis neque tempus et. Maecenas a massa placerat, laoreet nisl vel, venenatis diam. + +Phasellus at leo vel nisi aliquet placerat. Vestibulum luctus erat quis velit laoreet auctor. Aenean ultricies nulla tristique metus commodo, id fermentum justo tristique. Nullam ut tincidunt libero. Suspendisse volutpat, lacus ac congue ultricies, metus mi imperdiet magna, in maximus turpis ex eget leo. Sed lorem nibh, vestibulum id sodales ac, sagittis at elit. Curabitur purus nunc, sodales eget vehicula vitae, bibendum gravida diam. Nullam dignissim consequat pharetra. Nullam a diam consectetur, mollis odio sed, blandit lectus. Vestibulum eu velit id massa varius sagittis. Quisque tempor ante ac mauris rhoncus molestie. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. + +Ut sit amet euismod mi. Nulla facilisis est posuere, feugiat est et, dictum nulla. Proin eleifend ultricies nunc. Sed commodo justo nisi, id suscipit massa malesuada ut. Donec aliquam nibh tellus, vitae gravida lectus ultricies quis. Nam pulvinar lobortis erat sit amet convallis. Sed quis magna facilisis, tincidunt dui non, hendrerit nunc. Morbi egestas, risus fringilla fermentum porttitor, nunc velit viverra mi, non sodales augue arcu ac sapien. Duis blandit urna at nisl pellentesque semper. Nulla et malesuada nulla. Aenean tristique tortor odio, sit amet luctus odio aliquam id. Phasellus facilisis lorem vitae velit aliquam imperdiet. Cras faucibus dolor eget neque fringilla, ut mattis ex hendrerit. Integer molestie porttitor sagittis. + +Pellentesque diam quam, auctor eget tristique eget, molestie sit amet est. Pellentesque a eros non dui gravida volutpat. Donec molestie blandit nunc ac interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse lobortis, neque non aliquet convallis, lectus ex venenatis ex, quis malesuada massa erat non dolor. In tristique, enim eu ultrices ultricies, lectus ligula pretium orci, commodo cursus ante est vel odio. Sed quis accumsan purus. Nam fringilla ex ut urna vestibulum, et feugiat diam ultrices. Vivamus tempus felis ac quam blandit convallis. + +Vestibulum eros erat, volutpat in est at, blandit pharetra sapien. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean quis enim orci. Aliquam imperdiet vel arcu ac sagittis. Mauris vitae augue sed metus commodo ornare. Nulla malesuada tellus nisl, eu vestibulum ante mollis a. Sed sagittis euismod nunc, sit amet hendrerit tortor condimentum eu. + +Praesent lacinia massa eget mi auctor placerat. Fusce porttitor augue lectus, a cursus ante dictum vel. Vestibulum ultrices vel mauris in fermentum. Nunc tincidunt non magna sed pharetra. Donec porttitor rutrum arcu, vitae tincidunt lacus suscipit ac. Aliquam lorem mauris, pulvinar non dignissim sed, pulvinar vitae dui. Donec id neque eu velit imperdiet lacinia nec eu magna. Ut a purus sit amet nulla venenatis vulputate. Integer vulputate est sem, iaculis porttitor mi mattis et. Phasellus condimentum ipsum eget tellus viverra, a tincidunt nunc feugiat. Praesent posuere aliquam ex et faucibus. Nullam pretium felis id mauris luctus, a luctus eros sodales. + +Mauris et condimentum velit. Praesent id dignissim odio. Phasellus nisl velit, molestie sed nisi et, sollicitudin tempor nisi. Pellentesque lacus eros, ultricies non leo sit amet, porttitor ullamcorper ipsum. Vestibulum maximus lorem ac justo tempus imperdiet. Suspendisse rhoncus, mi in commodo tempus, orci turpis feugiat dui, nec facilisis arcu diam ut mauris. Vestibulum risus ligula, ornare non cursus vel, pellentesque non augue. Morbi eu gravida arcu. Nunc sed fermentum lacus. Nulla id quam aliquet, aliquet lacus in, rutrum metus. Duis tristique sodales risus vel interdum. Integer rhoncus nibh eget semper malesuada. Nunc sit amet ante diam. Fusce tincidunt aliquam ex, at lobortis tellus porttitor non. Vestibulum tincidunt iaculis dui vel scelerisque. + +Aliquam sagittis mauris eget massa accumsan viverra. Pellentesque luctus sit amet augue ac scelerisque. Praesent imperdiet nisi dolor, sed malesuada est commodo at. Aenean vel leo eget felis tincidunt interdum. Fusce orci mauris, egestas eget lectus et, finibus consectetur urna. Donec ut dapibus elit, eu lacinia neque. Ut et accumsan nulla. Sed ullamcorper ligula purus, eu dapibus nunc auctor vel. Ut convallis consectetur dapibus. Curabitur eget porttitor felis. Maecenas pretium ac leo vitae volutpat. Donec in augue sit amet lorem efficitur dignissim. + +Praesent iaculis tristique rutrum. Pellentesque id odio vel purus bibendum sodales suscipit id odio. Nullam ac velit imperdiet, imperdiet nisi sed, malesuada ipsum. Quisque varius dictum efficitur. Phasellus efficitur varius imperdiet. Aenean facilisis libero non augue porttitor, nec interdum felis imperdiet. Etiam et libero id elit commodo tincidunt. Nullam rutrum odio id rutrum tristique. Cras vehicula aliquet risus ac elementum. Duis nisl urna, commodo eget ante et, vehicula tempus lacus. + +Mauris eu sapien sed erat auctor volutpat vel vel tortor. Aenean in commodo felis. Donec a dui a urna varius aliquet quis at nisi. Pellentesque et urna lacinia, commodo arcu at, laoreet lectus. Aliquam sodales, massa in convallis aliquam, dui orci eleifend arcu, a gravida mauris magna sed arcu. Ut ac lectus in risus feugiat lobortis. Nulla quis est eget dui pharetra ultricies eget at risus. Phasellus sagittis molestie ligula, eget egestas orci volutpat vitae. + +Fusce nec finibus ligula, sed volutpat tortor. Sed placerat quam fringilla augue pharetra dictum. Proin ornare mi erat, eget sollicitudin ligula venenatis vitae. Aliquam semper sagittis urna rutrum pharetra. Vivamus lacinia mattis erat, vitae ultrices arcu. Maecenas id lacus eget justo imperdiet vehicula commodo a leo. Quisque vitae eros interdum, posuere ex ornare, tincidunt lectus. + +Vestibulum hendrerit sed libero et bibendum. Sed ornare eu massa ut vestibulum. Curabitur imperdiet odio felis, at ullamcorper eros rhoncus nec. Cras commodo nisl eu augue iaculis posuere. Aliquam massa tortor, consectetur quis dui in, mollis dictum tellus. Fusce porttitor dapibus arcu. Fusce finibus pretium porttitor. + +Proin dapibus viverra nisi. Cras ullamcorper purus et consequat fermentum. Duis imperdiet in dui in imperdiet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quis enim at ipsum ultricies auctor scelerisque nec nulla. Vivamus ut efficitur enim. Quisque dictum quam ac dui iaculis efficitur. Morbi at nulla convallis, condimentum tellus sit amet, dapibus nunc. Morbi metus felis, commodo sit amet justo id, finibus sagittis lorem. Nam nisi diam, fermentum dapibus varius in, convallis eu leo. Phasellus ut nunc orci. Sed tincidunt mauris in ante consequat, id bibendum libero volutpat. Aliquam a dictum libero. Etiam massa odio, congue ut lorem tincidunt, elementum egestas ligula. + +Ut semper arcu a lectus interdum euismod. Curabitur nec ultrices neque. In eget sapien nulla. Pellentesque pellentesque faucibus urna id placerat. Aenean condimentum posuere interdum. Etiam vel tristique lorem, in dapibus urna. Vestibulum facilisis lobortis metus ac egestas. Vestibulum ultrices aliquet dui id efficitur. Sed a velit sed erat ultrices sodales suscipit a tortor. Nam mattis rhoncus augue et viverra. Praesent volutpat gravida enim quis sodales. + +Nam placerat nisl a ullamcorper pharetra. Sed eu eros egestas, suscipit ante id, efficitur mi. Curabitur accumsan gravida pellentesque. Vestibulum urna risus, condimentum vel libero in, porta pharetra nisi. Duis eu feugiat neque, quis condimentum dolor. Suspendisse et elementum urna. Vivamus malesuada nisi eget blandit faucibus. Duis eu lorem ac est ultrices placerat nec nec elit. Nunc sed sagittis ligula. Vivamus gravida suscipit tellus nec euismod. + +Ut posuere porta diam, vitae euismod erat egestas vitae. Aenean imperdiet quis quam eget dictum. Cras vulputate elit eu nibh scelerisque, vitae consectetur nisi malesuada. Praesent iaculis, neque nec tempor elementum, est mi egestas urna, nec commodo neque lacus vel mi. In a orci eu metus elementum tincidunt nec id tortor. Aenean augue augue, vulputate a porta quis, bibendum finibus augue. Nam condimentum ante ac congue ultrices. Praesent eu nisi eu enim accumsan scelerisque et id augue. Cras gravida dictum suscipit. Nulla tristique tempor lacus non eleifend. Curabitur sodales est in arcu accumsan, vel dignissim nunc blandit. Aenean sodales sodales lectus volutpat commodo. Maecenas venenatis accumsan nibh, sit amet semper risus ultrices non. + +In blandit iaculis dolor sit amet convallis. Aliquam quis nisl sit amet augue semper vehicula. Sed aliquam vel ex vel condimentum. Nunc diam massa, mattis ac felis vel, cursus tincidunt ligula. Aliquam erat volutpat. Quisque faucibus in metus in tempus. Ut pharetra congue tellus. Vivamus est libero, fringilla vel elit ac, rhoncus fermentum arcu. Praesent tortor diam, mattis in varius commodo, lacinia accumsan neque. Integer nec luctus nibh. Duis tincidunt velit nisi, id porttitor turpis posuere in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam finibus tortor lectus. Curabitur condimentum orci eget urna sollicitudin vehicula. + +Donec sagittis mi lacus, quis rutrum sapien scelerisque sed. In quis interdum velit. Nulla eget tincidunt enim. Fusce viverra, sem pharetra ultricies laoreet, magna erat ornare lectus, a viverra mauris magna id mi. Quisque vitae pretium velit. Integer venenatis vel sapien non varius. Praesent eros neque, posuere sit amet posuere ut, posuere a sem. Vestibulum porttitor interdum posuere. Nam viverra felis dolor, eget ultrices lacus tincidunt a. Suspendisse elementum rhoncus tristique. Nam vehicula, odio eu porta ullamcorper, neque nunc pretium neque, ac vehicula mauris eros ac turpis. Aliquam augue nisl, pharetra non mauris id, finibus egestas massa. + +Aliquam rhoncus tortor a nunc vulputate gravida. Phasellus aliquam lorem ipsum, a suscipit orci euismod ac. Curabitur fringilla orci in ante aliquam venenatis. Ut nec sollicitudin orci. Morbi consectetur massa nec lacus vestibulum commodo. Donec quis erat at nibh scelerisque interdum. Donec sed velit molestie purus volutpat tempus. Aenean consequat, massa vitae mollis eleifend, felis ante convallis ex, quis egestas libero nisi interdum dui. Maecenas aliquet nisi quis est dapibus posuere. + +Phasellus lectus ex, finibus non orci et, suscipit fermentum orci. Vestibulum sed ligula non arcu facilisis feugiat. Praesent pellentesque eros quis eleifend tempus. In hac habitasse platea dictumst. Nulla accumsan suscipit risus, nec dignissim purus sollicitudin quis. Vestibulum vestibulum ligula non massa congue commodo. Aliquam velit ante, facilisis et aliquet non, imperdiet nec velit. Nunc vel elit felis. + +Sed sed ex ut dui cursus consectetur. Phasellus laoreet velit lacinia dui placerat tincidunt. Nullam ornare sagittis quam ac pretium. Donec imperdiet velit quis ipsum placerat, vitae lacinia felis sagittis. Aenean vitae dui fermentum, laoreet lacus egestas, faucibus libero. Maecenas blandit blandit mi, et mattis lectus placerat sollicitudin. Aliquam at semper nulla. + +Sed scelerisque lacus felis, et commodo libero tincidunt ac. Ut vel elit vel ex luctus lacinia ut et nisi. Sed ac tristique nisl. Suspendisse efficitur varius purus, sit amet gravida orci sagittis lacinia. Proin non placerat urna. Duis vehicula faucibus est vitae vehicula. Praesent vehicula tempor eros, in aliquet nisl vehicula in. Phasellus in nibh commodo, tempor magna in, convallis metus. Vivamus velit risus, scelerisque quis dolor in, finibus rhoncus erat. Vivamus ipsum libero, tempus non magna eget, condimentum tempus elit. + +Sed eu feugiat neque. In velit ex, suscipit in semper blandit, malesuada in orci. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec sem odio, elementum at turpis in, aliquet posuere augue. Etiam accumsan libero lorem, tempor cursus purus fringilla in. Vestibulum id diam consectetur, interdum dui vitae, accumsan tellus. Ut eu viverra nisi. Duis odio nisl, consectetur id volutpat eu, interdum a tortor. In et ipsum interdum, fringilla urna nec, congue lectus. Aliquam eu sodales neque. Vivamus et tincidunt dolor. Sed porttitor rhoncus rutrum. Nulla facilisi. + +Vivamus dapibus ipsum vitae libero ullamcorper, quis ullamcorper tortor porttitor. Phasellus elementum sapien ac felis sagittis, non finibus massa faucibus. Curabitur id enim neque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aenean velit lectus, tempus placerat pretium ultricies, mollis sit amet nibh. Praesent tincidunt turpis purus, vitae malesuada sapien eleifend at. Pellentesque velit mauris, volutpat auctor pharetra at, laoreet vel mi. Duis a ornare leo, nec malesuada ante. Donec a felis nec ex varius rutrum at a libero. + +Etiam blandit nulla et lorem viverra, vitae suscipit mi luctus. Etiam enim nisl, dignissim eget lectus a, molestie hendrerit leo. Cras placerat leo nec blandit aliquet. Suspendisse id cursus metus. Aliquam a lobortis lectus, eget consequat erat. Praesent congue nulla vitae convallis pulvinar. Donec sed dui tellus. Aenean vehicula neque malesuada mi malesuada, sed lobortis nisl porttitor. Sed eu felis lacinia, fringilla nibh ac, laoreet ex. Vestibulum nibh ex, sagittis eu bibendum et, laoreet ut lectus. Proin ac augue tellus. Nulla tristique metus ut sem egestas sodales. In lorem sapien, tempor sit amet semper a, dignissim a dolor. + +Mauris finibus justo ut pretium vestibulum. Morbi euismod faucibus fringilla. Curabitur vitae dictum ipsum. Curabitur nec nulla fringilla, laoreet ligula eu, convallis magna. Proin in accumsan sem. Morbi pretium venenatis sem, vitae fringilla leo vestibulum et. Maecenas justo ligula, iaculis a finibus nec, aliquam tempor ipsum. Donec cursus nisi vel purus pulvinar, non interdum nulla semper. In eu ullamcorper odio. Sed ac augue ut urna pulvinar rhoncus. Integer maximus ultrices nisl, nec volutpat tellus facilisis eu. Fusce dictum, leo iaculis egestas consectetur, enim ligula aliquam nunc, sed condimentum neque dui eget nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; + +Fusce vitae orci eu purus vehicula viverra. Vivamus mollis orci sed euismod sagittis. Duis dui sapien, ullamcorper in gravida nec, imperdiet sed purus. Cras ligula nulla, consectetur a urna a, luctus ultricies augue. Aliquam tincidunt, lectus eget auctor venenatis, elit tortor malesuada mauris, sed iaculis lectus libero et lectus. Aenean dictum imperdiet tortor, ac aliquet magna rhoncus sed. Mauris facilisis velit suscipit ligula tristique ullamcorper. Praesent leo mauris, rhoncus eu sodales a, lobortis nec nibh. + +Cras in libero felis. Donec luctus nunc id imperdiet consectetur. Nam ultrices suscipit mi, eu pretium urna luctus eget. Phasellus eu lacinia augue. Proin eu est condimentum ligula volutpat semper. Sed luctus, dolor quis bibendum venenatis, neque nibh condimentum felis, vitae cursus libero velit vitae lorem. Donec ultricies ullamcorper ipsum. Maecenas maximus accumsan blandit. + +Mauris aliquet, ex non facilisis tristique, nibh elit efficitur quam, et gravida sapien leo sed diam. Suspendisse malesuada odio vel lorem dignissim, eu accumsan ante egestas. Vivamus blandit erat sed fringilla euismod. Etiam nec mauris a sem finibus dapibus. Quisque hendrerit eros nec mattis ultricies. Vestibulum blandit nulla a eleifend sollicitudin. Fusce hendrerit, nunc ut cursus fermentum, arcu odio laoreet turpis, a tincidunt purus massa nec sem. Nam id tellus et eros vehicula fermentum. Nullam imperdiet rhoncus lectus, at vestibulum nunc semper luctus. Sed a massa sed urna posuere congue in sed augue. + +Nullam condimentum eget tortor in lobortis. Maecenas ac cursus tellus. Nunc mollis lorem risus, sed tincidunt sapien ullamcorper quis. In nec diam quis ligula euismod feugiat vitae eget dui. In pulvinar, arcu in molestie sodales, augue elit aliquam elit, vel dignissim quam mi maximus quam. Sed condimentum, nibh ut finibus faucibus, diam leo ultrices dolor, quis cursus nunc dolor non urna. Aliquam suscipit, magna vitae gravida porta, sem orci mattis arcu, nec fringilla dolor nunc in purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nunc egestas cursus magna in ultrices. Maecenas quis laoreet ex, eu vehicula metus. Donec sed congue sem, in vulputate diam. Pellentesque molestie nulla ipsum, nec dignissim enim ultricies eget. Morbi vehicula odio ut justo tempus blandit. Sed nec condimentum elit. Morbi ut facilisis mauris. Aliquam luctus odio sed ante aliquam, eget venenatis risus luctus. + +Integer laoreet odio a tellus tincidunt auctor sed id dolor. Praesent quis velit quis nunc dignissim iaculis non non lectus. Praesent blandit ligula urna, semper molestie lectus dignissim sed. Suspendisse bibendum, leo sed placerat tincidunt, sapien dui molestie dui, elementum dignissim nisl nisi et nulla. Ut feugiat felis id malesuada hendrerit. Pellentesque ut nisi et ipsum laoreet tempus vel non eros. Cras ut ante mi. Fusce sed maximus lacus. Etiam hendrerit, odio in maximus tincidunt, felis dolor malesuada justo, quis porttitor odio ipsum vitae eros. Vestibulum risus ante, iaculis sodales accumsan eget, tempor quis neque. + +Vestibulum eget elit vestibulum, imperdiet ex ut, cursus metus. Proin at interdum leo. Vivamus a nisl tristique, varius nisl dignissim, auctor leo. Donec arcu felis, condimentum vel pharetra vitae, fringilla at dolor. Integer elementum viverra tortor, a ullamcorper nunc bibendum in. Vivamus et arcu sit amet nulla maximus condimentum. Vestibulum in nisi ut nulla sollicitudin gravida. Aliquam nulla ipsum, venenatis eu fermentum id, sodales vel diam. Suspendisse metus mi, facilisis ornare est et, interdum pretium odio. Morbi eget nunc orci. + +Mauris neque dolor, imperdiet non dolor ut, suscipit lacinia mi. Donec dolor mauris, viverra in purus aliquet, tincidunt volutpat mi. Proin at dapibus dolor, vel egestas eros. Nulla mattis dictum iaculis. In pulvinar dui sem, eu tincidunt ligula sodales eget. Proin consectetur augue a libero suscipit rutrum blandit id eros. Pellentesque lorem erat, porta at felis id, congue malesuada urna. Quisque fringilla ut odio sed porta. Quisque congue lorem nec augue luctus varius. Nullam nec metus fermentum lacus egestas pharetra a volutpat lectus. Fusce euismod eros sit amet nisi semper imperdiet. Donec a viverra libero, vel ultrices felis. Aliquam vitae ante quis elit posuere ultricies. Mauris velit purus, tincidunt sit amet velit sit amet, sollicitudin pharetra odio. + +Donec semper eleifend aliquet. Vestibulum fringilla augue non arcu tristique pellentesque. Duis viverra, eros vitae dignissim lobortis, mauris lorem ultricies tellus, non cursus diam tellus vitae ipsum. Ut et arcu turpis. Fusce eget neque cursus, posuere augue interdum, fringilla libero. Donec commodo velit finibus urna pellentesque blandit at eu turpis. Proin et viverra tellus, a pharetra sapien. Ut a odio fringilla, viverra elit in, dictum tortor. Morbi est diam, sagittis sed pulvinar sit amet, dictum at lorem. Phasellus a condimentum massa, sit amet vestibulum purus. Suspendisse quis pharetra tortor. Nunc tempus magna vitae ligula luctus laoreet. Integer eleifend varius commodo. In hac habitasse platea dictumst. Cras eget metus sapien. Nulla facilisi. + +Cras euismod mauris tortor, a dapibus ligula gravida fermentum. Duis ultricies fermentum faucibus. Sed interdum, lacus vel mollis tempus, enim tellus ultrices nisi, in sollicitudin enim purus non nulla. Sed eget quam massa. In hac habitasse platea dictumst. Aenean at ante metus. Sed eleifend luctus ipsum nec lacinia. Vestibulum facilisis sodales dui, nec molestie neque tempus in. Curabitur consectetur tortor eget ipsum eleifend varius. Aenean finibus nulla at velit luctus, sed finibus ipsum semper. Vivamus turpis nisi, vulputate in pellentesque ultrices, rhoncus id augue. Quisque efficitur semper ligula, sed dictum turpis porta vitae. Aliquam malesuada est ac leo fermentum, et porttitor erat sagittis. + +Morbi felis odio, tristique quis tempor at, convallis commodo lectus. Integer tincidunt lacus dolor, id molestie ante luctus non. Fusce nec quam in quam euismod malesuada. In consectetur magna ut fermentum volutpat. Phasellus malesuada risus nunc, non pellentesque mauris aliquet quis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent eget mi metus. Nunc in risus eget lacus gravida tristique a in nisi. Cras consequat aliquam quam vitae pulvinar. Curabitur commodo purus ligula, et ornare ipsum aliquet at. Sed tempor sed enim ut convallis. Mauris cursus magna non diam dapibus euismod. Nullam ac nisl est. Maecenas aliquet quam erat, ac imperdiet elit fermentum at. + +Sed urna arcu, convallis et malesuada sit amet, iaculis quis felis. Fusce pellentesque tincidunt lacus, quis aliquam enim dictum vitae. Suspendisse potenti. Donec ut tincidunt est, eget iaculis leo. Curabitur auctor pharetra augue, sed egestas ante varius id. Etiam sollicitudin et mauris vitae ullamcorper. Maecenas mollis vulputate viverra. Etiam efficitur, metus quis cursus elementum, felis arcu congue dui, et volutpat augue tellus a dolor. Duis rhoncus molestie tincidunt. Nunc finibus tortor ut nunc vehicula, ac vestibulum velit tristique. Donec in eros ut erat tempor tincidunt. + +Pellentesque cursus leo non nisl posuere, ac tincidunt lorem tempus. Praesent ut erat dictum, tincidunt elit ut, varius risus. Sed hendrerit id elit ut vestibulum. Suspendisse consequat metus sit amet neque dictum, sed feugiat risus egestas. Aliquam lobortis nisl elit, eget posuere ligula aliquam eget. Nullam lobortis a nunc vel malesuada. Praesent venenatis nisl sit amet libero suscipit, ut placerat sapien egestas. Cras condimentum justo sit amet massa sollicitudin, ac ultricies metus dignissim. Morbi mauris nunc, varius a ornare sit amet, pretium ut ex. Etiam sollicitudin, risus ut viverra euismod, magna mauris mattis tortor, eget cursus massa odio eu ipsum. Mauris tempus nunc mattis lectus varius cursus. Curabitur nisi erat, vulputate rutrum scelerisque vitae, convallis non lorem. Suspendisse purus nulla, aliquet eget hendrerit dignissim, malesuada nec orci. + +In sagittis elit id augue iaculis euismod. Maecenas consequat odio sit amet massa elementum, eget fermentum velit varius. Aliquam ac tellus ac ex ullamcorper tincidunt eget eget diam. Quisque diam tortor, vehicula ac sollicitudin vitae, sollicitudin efficitur ligula. Nullam ut rutrum quam. Phasellus ornare posuere felis, sed vehicula ipsum blandit quis. Etiam a purus eu tortor interdum rutrum. Quisque sed tincidunt magna. Morbi sodales mi vitae sem cursus, sed venenatis augue porttitor. Nam posuere enim dictum hendrerit bibendum. Ut facilisis, dolor sed vestibulum ornare, tellus elit suscipit leo, et euismod arcu neque at tortor. Suspendisse pulvinar neque vel porttitor vestibulum. + +Suspendisse in metus ut nibh euismod sodales. Sed tempor eget dolor at semper. Suspendisse at urna lacus. Donec quis velit sed elit ultricies vestibulum quis nec ipsum. Duis at augue et turpis gravida rhoncus quis in est. Fusce sit amet malesuada quam. Integer nec augue non nisl consequat scelerisque eu et velit. Sed vitae enim felis. + +In a sem accumsan, iaculis nulla vitae, ultricies turpis. Nulla luctus, ligula a gravida dapibus, mauris mauris rutrum erat, et lobortis libero libero sed nibh. In quam diam, dapibus vitae diam in, interdum accumsan ligula. Phasellus ac diam mollis, laoreet sapien ut, vehicula quam. Donec cursus elit tortor, vel mattis odio ornare ut. Quisque et justo a purus aliquam laoreet. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla non euismod metus. Integer venenatis eu nisl tempus consectetur. + +Phasellus dictum elit vel velit rhoncus, porttitor tempor mauris scelerisque. Quisque nec fringilla erat. Sed consectetur in eros ac maximus. In nec lorem sapien. Pellentesque aliquet bibendum mi, at pulvinar justo mattis nec. Proin justo lorem, tempus nec elit lobortis, interdum pretium nisl. Pellentesque euismod, massa a consectetur dignissim, risus purus dictum risus, in molestie dolor elit in turpis. Cras vitae dapibus augue. + +Proin enim diam, semper ac dapibus eget, vulputate id ligula. Proin lectus diam, pharetra sed turpis non, varius pharetra eros. Quisque eget rhoncus enim. Integer velit ante, molestie eget convallis vitae, laoreet eget massa. Etiam at sem nec urna accumsan convallis. Nam a diam luctus, scelerisque nisi id, pulvinar quam. Aliquam convallis maximus aliquet. Aliquam at diam nec tellus pretium euismod. Cras aliquam justo nec quam scelerisque vulputate. Etiam dictum eleifend elit elementum consequat. Donec semper tempus ultrices. Pellentesque bibendum vitae dolor vel scelerisque. Aenean lacinia hendrerit dolor non congue. + +Ut congue orci turpis, sit amet ultricies orci luctus in. Ut felis odio, vestibulum non convallis sit amet, congue vitae mauris. Nullam blandit enim vel lorem laoreet, at gravida est sollicitudin. Aenean posuere dignissim ex, id varius arcu iaculis id. Vestibulum id nulla eget magna pulvinar rutrum. Suspendisse pulvinar blandit mauris, vel pharetra turpis finibus a. Quisque ac ligula arcu. Praesent semper nulla sed ultrices scelerisque. Quisque id erat eget odio dictum euismod. Donec sit amet nunc purus. Quisque nulla dui, sollicitudin non odio sit amet, sagittis interdum urna. Nunc feugiat, lacus non commodo volutpat, tellus lorem fermentum risus, eget dapibus urna massa a elit. + +Sed id tellus augue. Donec quis fringilla lacus. Integer suscipit faucibus eleifend. Donec lobortis odio ut felis cursus rutrum. Morbi augue erat, rutrum eu nisl sed, tincidunt porta enim. Nulla consequat malesuada tellus. Pellentesque facilisis vel nibh et pretium. Morbi volutpat ante sed leo tincidunt, egestas bibendum dui auctor. Morbi mattis feugiat maximus. Donec a sagittis ante, non euismod metus. Morbi commodo neque viverra pretium fermentum. + +In sodales, nisl quis vulputate luctus, sapien est fringilla elit, sed vestibulum urna libero ac ante. Suspendisse potenti. Duis eget sagittis elit. Mauris sapien ligula, egestas at auctor eu, efficitur at nisi. Proin elementum, erat nec tincidunt laoreet, elit risus pellentesque sapien, in malesuada enim ligula id magna. Proin scelerisque augue lorem, et hendrerit ante fringilla vel. Quisque in faucibus nunc, sit amet convallis diam. Sed fermentum tristique fringilla. In condimentum purus ornare tristique dapibus. In malesuada nunc lorem, vel imperdiet erat pellentesque id. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Quisque feugiat faucibus nulla, quis faucibus velit lobortis in. Donec augue sem, scelerisque vitae tortor ac, aliquet fermentum nulla. Fusce convallis non metus in ultrices. Cras justo arcu, tristique vel libero sed, fermentum ullamcorper justo. Mauris libero erat, elementum nec malesuada ac, commodo eget ante. + +Duis laoreet diam non orci volutpat rhoncus. Sed bibendum dolor quam, eget sagittis enim tincidunt at. Mauris at varius sem, id luctus augue. Sed venenatis pulvinar viverra. Curabitur enim nisi, mollis at fermentum ac, rhoncus iaculis mi. Ut dictum urna velit, a rhoncus risus tempus ut. Cras tristique scelerisque dignissim. Donec ex felis, dictum at eleifend at, posuere bibendum quam. Donec luctus aliquet velit, id fringilla sem tincidunt sed. Quisque cursus imperdiet diam, ut facilisis augue convallis et. Aliquam hendrerit consectetur neque, vitae ultricies nulla aliquam ut. Donec at justo ut ipsum aliquam bibendum in id ante. Aenean fermentum eros vel turpis tristique egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent vitae dui felis. + +Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam faucibus leo id mi blandit, posuere maximus felis malesuada. Phasellus et porttitor magna. Curabitur leo ipsum, malesuada at lobortis finibus, facilisis id purus. Suspendisse potenti. Suspendisse at iaculis metus. Nam pharetra leo quis ex aliquam fermentum. Sed quis metus faucibus, varius nunc id, condimentum est. Nam lacinia quis velit a iaculis. Nullam accumsan mattis neque vitae posuere. Vivamus sem neque, ultrices sed molestie at, gravida ut est. Nunc a tellus viverra felis pulvinar fermentum vitae nec mi. Nulla et hendrerit magna, sed bibendum mauris. Cras eget diam eu augue convallis porttitor eget sit amet tortor. Cras arcu tortor, vulputate vitae erat non, rutrum rhoncus urna. Donec blandit non erat sit amet gravida. + +Sed feugiat in nibh et sagittis. Quisque in maximus mi, eu elementum neque. In hac habitasse platea dictumst. Pellentesque ultricies consectetur urna vitae imperdiet. Nullam velit lectus, laoreet ut sem eu, commodo fringilla ipsum. Vivamus placerat vulputate ipsum nec viverra. Aenean vel venenatis augue, vitae pharetra felis. + +Pellentesque rutrum urna orci, a condimentum mi ultrices quis. Nunc facilisis velit nec velit eleifend vestibulum et vel erat. Fusce consequat ex ut lacus elementum lacinia. Nulla a sapien ut ex dignissim pulvinar sed vel ex. Aenean porta diam sit amet pellentesque dignissim. Vestibulum mollis convallis auctor. Etiam lacinia eros non nulla blandit tristique. In hac habitasse platea dictumst. Vestibulum dapibus iaculis consectetur. Morbi ex odio, posuere at sollicitudin mattis, efficitur pharetra sapien. Etiam placerat nec quam vitae fringilla. Donec sodales bibendum odio, eget pharetra erat efficitur id. Nullam ultricies dui odio, sit amet tincidunt eros vestibulum eu. Donec semper libero in lacus elementum maximus. + +Curabitur commodo ex nec sapien fermentum suscipit. Donec vel erat placerat, convallis dui non, mattis mauris. Donec placerat dui augue, et dignissim justo feugiat id. Phasellus nec justo ex. Phasellus eget lobortis orci. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras in scelerisque neque, non pretium purus. Aenean rutrum libero at fermentum pharetra. Nam elementum felis nec dapibus dignissim. Quisque ultricies ipsum a odio pellentesque mollis a maximus leo. + +Sed id urna hendrerit, convallis nisl quis, tristique felis. Sed eget consequat nisl. In vel tincidunt erat. Vivamus molestie rhoncus libero, non tincidunt lacus elementum non. Aenean faucibus lacinia nisi ac lobortis. Suspendisse iaculis augue nibh, id varius tortor ornare ac. Duis vitae congue nibh. Praesent at tortor et justo aliquam fringilla vitae id orci. Mauris ultricies velit condimentum lectus volutpat, vitae tincidunt odio fermentum. Curabitur luctus convallis libero eget pellentesque. Vivamus a tempus ipsum. + +In et justo vel nisi convallis tempor ac vitae purus. Cras efficitur ac orci sit amet aliquet. Vestibulum vel libero egestas, pharetra nisl a, ultrices erat. Nunc id turpis a erat aliquet hendrerit. Suspendisse lobortis est sed quam consectetur condimentum. Proin fermentum purus nec risus dignissim lacinia. Quisque eu libero eu nibh dictum congue id at odio. Nulla posuere justo a mollis scelerisque. Nunc luctus, augue congue volutpat tincidunt, orci nulla euismod elit, eget mollis arcu augue eget elit. Aliquam rhoncus nisl at quam dictum viverra. + +Pellentesque sit amet elit condimentum, suscipit sapien sed, dapibus turpis. Proin felis nunc, aliquet in vulputate a, lobortis et ex. Donec ac magna vulputate, tincidunt mauris eget, ultrices urna. Duis venenatis commodo massa, eget rutrum enim consectetur vel. Aenean vel erat hendrerit, tincidunt dui ut, elementum est. Vestibulum in ipsum fringilla, gravida nunc ac, sagittis dolor. Aenean pulvinar ornare diam eget ultricies. + +Cras ut luctus mauris, sed sodales orci. Quisque vitae ullamcorper metus. Ut vel justo ligula. Aenean sit amet tellus tortor. Mauris eu diam et mauris vestibulum vehicula. Donec finibus, turpis vel blandit pretium, sem quam sagittis purus, in sagittis nibh leo id augue. Duis venenatis mollis pretium. Praesent pretium bibendum eros. In non ipsum cursus, tristique orci vel, elementum dolor. Nulla egestas leo in feugiat dignissim. Integer fringilla odio ut aliquam accumsan. + +Sed risus est, tristique ut ex quis, aliquam malesuada lorem. Maecenas hendrerit eros ultricies venenatis aliquam. Vestibulum ut laoreet lectus. Integer purus neque, porttitor sed tristique congue, vestibulum et ligula. Aliquam fringilla, eros et mattis vulputate, tellus urna auctor velit, ac pharetra ligula mauris ut tortor. Donec tristique nunc metus, vitae vulputate nulla iaculis vitae. Donec iaculis dapibus dolor, eget rhoncus dui. Ut feugiat sed enim tristique efficitur. Curabitur leo risus, vehicula ac ligula id, vestibulum eleifend diam. Aliquam erat volutpat. In ipsum diam, volutpat at diam non, finibus lobortis eros. Curabitur id diam mi. Proin purus urna, auctor et diam nec, aliquam interdum lectus. Ut eget mollis tortor. Quisque elementum porta ultrices. + +Nullam aliquet augue velit, sed suscipit erat eleifend non. Vivamus nisl felis, blandit sit amet neque id, malesuada tincidunt mi. Phasellus a mauris metus. Cras tempor, arcu tincidunt fermentum viverra, tortor lacus tincidunt erat, vel tristique dolor justo vitae eros. Aliquam erat volutpat. Cras aliquet nunc et dignissim sagittis. Fusce vel nisi mi. + +Nunc tempus purus non magna tincidunt, eget dignissim justo posuere. Aenean mattis lacinia risus vel luctus. Suspendisse ac rhoncus massa, id finibus dui. Pellentesque nulla turpis, iaculis vitae mauris non, hendrerit tempus erat. Integer venenatis, dui in rutrum porttitor, purus risus commodo nisl, a fermentum nisi nunc eu neque. Quisque euismod est nec mi facilisis, ut varius leo congue. Integer sed arcu ultrices, volutpat diam at, elementum turpis. Quisque et accumsan orci. Duis consequat sollicitudin tortor in ultrices. Vestibulum porta fringilla auctor. Maecenas maximus eros at erat vestibulum mattis. Praesent fringilla pellentesque quam, vel ullamcorper nisi sollicitudin non. Curabitur fermentum fermentum ligula sed viverra. + +In hac habitasse platea dictumst. Quisque et convallis est, quis posuere felis. In congue, elit nec venenatis hendrerit, eros sapien dictum erat, non vehicula nibh felis ac sem. Nam sed semper massa. Proin id accumsan lorem. Mauris ultrices leo et velit euismod facilisis. Nulla facilisi. Morbi ultrices, mauris id ullamcorper sodales, ex neque eleifend tellus, sed luctus neque orci a dui. Curabitur ut eros metus. Morbi rhoncus odio eget lacinia blandit. Aenean lobortis consequat imperdiet. Proin tempus vehicula massa, nec posuere ex. Phasellus convallis, felis ac lacinia luctus, purus nunc imperdiet ante, eu hendrerit nibh diam eu lacus. + +Vivamus ullamcorper molestie turpis, et euismod lorem semper ac. Proin ornare, purus at ullamcorper euismod, lacus odio gravida nisl, vitae pretium mi erat a sapien. Duis ultrices libero turpis, sit amet varius sapien tempus in. Integer eget dignissim est. Aenean eget nulla nec libero faucibus tempus. Etiam in pellentesque risus. Nunc sed luctus lacus. Duis tristique nulla non enim consequat, congue vestibulum nisl interdum. Proin faucibus, eros non accumsan rutrum, ipsum justo fermentum augue, tempor ornare est metus sit amet tellus. Duis malesuada vel justo at finibus. Nullam sit amet enim scelerisque, dapibus velit ut, iaculis lectus. Nunc elementum erat nibh, eget finibus dolor porta in. Fusce varius tellus mattis tellus commodo pellentesque. Cras viverra gravida ligula, quis hendrerit ex posuere vitae. Sed quis tempor felis, ultrices faucibus velit. + +Quisque porttitor mi vitae metus dapibus, eu tincidunt turpis pharetra. Fusce dolor nulla, vulputate a ligula sed, suscipit hendrerit sem. Integer id nunc vitae erat dictum tempor. Morbi leo dui, tincidunt sed dapibus non, vestibulum sit amet magna. Proin et mauris tellus. Nam volutpat orci eu eros imperdiet congue. Suspendisse nec dictum magna. Nunc consectetur varius augue a ultricies. Proin nec lacus eget massa mattis ornare eget id ligula. Sed laoreet ante nec efficitur lacinia. + +Integer maximus laoreet tellus, eget aliquam mauris luctus ut. Sed condimentum lectus et mi eleifend egestas. Integer convallis tempus sem laoreet consequat. Suspendisse gravida commodo purus eu consequat. Pellentesque sed mi efficitur, tristique felis et, tempus lacus. Nullam in dictum est. Aenean libero libero, ullamcorper a cursus eget, mattis vitae diam. Aliquam at dolor turpis. + +Ut mauris justo, accumsan molestie eros vitae, interdum ornare lorem. Mauris ut consectetur nulla, cursus vulputate lectus. Donec commodo urna velit, nec lacinia elit accumsan sit amet. In et augue vel leo rutrum vulputate at vel magna. Phasellus in tempus erat, quis elementum mauris. Vestibulum tincidunt facilisis ante, ut volutpat eros rhoncus vitae. Aliquam erat volutpat. Proin sit amet lacus turpis. Etiam maximus sodales libero, ut vestibulum dui lacinia sit amet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec scelerisque, turpis et auctor dictum, dolor odio sodales ligula, a convallis dolor erat a augue. Sed felis nisi, tristique sit amet dolor sit amet, imperdiet auctor mauris. Aenean vitae ex molestie, suscipit lorem fermentum, laoreet nisi. + +Suspendisse pellentesque facilisis erat, sed faucibus sapien facilisis et. Proin pharetra augue ut lorem viverra pulvinar. Fusce quam neque, egestas sed est non, rutrum porta justo. Duis fermentum tincidunt ipsum non pharetra. Suspendisse potenti. Ut volutpat magna quis erat vehicula posuere. Aliquam consequat consequat gravida. Etiam dictum gravida semper. In vitae maximus felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. + +Aenean pretium, nisl ac dapibus lobortis, sem justo fermentum magna, in ultricies ipsum felis vitae ex. Pellentesque dictum eget purus sit amet commodo. Nunc dapibus, leo eget bibendum luctus, leo nibh eleifend lectus, eget imperdiet felis sem id nunc. Nullam egestas justo commodo, blandit ante at, placerat dolor. Nunc malesuada nulla et nisi maximus, quis molestie diam blandit. Quisque elementum lacus purus, quis varius lectus sodales finibus. Donec sodales nunc non nunc tincidunt, eu rutrum arcu pretium. Etiam risus odio, consequat eget quam eu, dignissim iaculis erat. Aliquam eu porttitor urna. Donec molestie, diam quis tempus maximus, leo mauris pretium mauris, ut lobortis est ipsum a mauris. Nam dignissim congue leo, id mattis sem efficitur eget. Proin risus lorem, fringilla vel varius ac, hendrerit vel neque. Vestibulum auctor est fermentum, congue purus at, semper dolor. Maecenas nec nunc at dolor blandit suscipit. Donec eu nisi aliquam, eleifend sem a, ultrices ex. + +In at augue risus. Nam sed justo in quam porta maximus. Praesent elementum tellus sed dui gravida hendrerit. Cras ultricies pulvinar lobortis. Maecenas tortor augue, auctor ut eleifend eget, egestas at lectus. Aliquam erat volutpat. In in egestas tellus. + +Quisque volutpat placerat vulputate. Nulla aliquam consectetur ex a vulputate. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla volutpat leo elit, id blandit erat vehicula eget. Pellentesque molestie, lacus ac facilisis fermentum, turpis enim faucibus felis, sit amet rhoncus libero dui at mauris. Proin vel placerat risus. Nullam eleifend orci eget eros tempor, aliquet semper diam malesuada. + +In convallis gravida laoreet. Praesent scelerisque mollis massa, non lobortis sapien elementum id. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Etiam dignissim varius eros vitae molestie. Aenean molestie nisl ac bibendum laoreet. Sed placerat pellentesque augue, sit amet commodo turpis condimentum vitae. Duis a purus placerat, tempor nulla ac, rutrum ipsum. Donec lectus nisi, laoreet quis diam eget, finibus scelerisque ante. Aliquam erat volutpat. Integer convallis justo vel nibh vehicula molestie. Donec ut mi eget est pulvinar euismod. In id ligula ut enim venenatis fermentum. In metus leo, rutrum ac mollis tincidunt, semper quis lacus. Sed lobortis, augue in condimentum suscipit, augue velit tempor leo, eu mollis turpis lectus auctor orci. + +Nullam efficitur erat ut dolor tristique condimentum. Suspendisse pretium ex ut bibendum ullamcorper. Phasellus dapibus enim sed tellus condimentum, sit amet mattis tellus cursus. Phasellus venenatis augue in lacus suscipit, vel hendrerit felis vehicula. Cras consectetur cursus lorem vulputate cursus. Vestibulum nec auctor sem. Nam ultricies, mi a ullamcorper eleifend, enim magna elementum erat, a accumsan turpis nunc et tortor. Donec venenatis maximus lacus vitae sodales. Donec dapibus rutrum porta. Nulla facilisi. Vivamus aliquam ex vitae sem consequat blandit. Nullam ultricies, ante ac rutrum efficitur, magna ligula maximus turpis, ut porta est velit et magna. Vestibulum mauris massa, posuere nec convallis in, maximus a nulla. Mauris vitae lorem sed nulla aliquam tempor. Fusce vel fringilla metus, ac aliquet metus. + +Ut ac mattis augue. Fusce cursus at quam ut vehicula. In laoreet cursus urna eu fermentum. Suspendisse est mauris, gravida interdum urna a, ullamcorper hendrerit arcu. Donec dapibus blandit massa nec vulputate. Nam a lacus pretium, imperdiet risus ac, aliquam nunc. Nulla facilisi. Vivamus quam libero, vestibulum sed tellus eget, ornare gravida dolor. Sed placerat nulla in velit imperdiet mattis. Sed at arcu eleifend, scelerisque urna non, porta massa. In volutpat commodo quam et sollicitudin. + +Donec in magna ullamcorper, auctor ex malesuada, tincidunt dui. Etiam enim risus, cursus sit amet ante ut, blandit tincidunt purus. Etiam rutrum dolor nulla, vel feugiat quam convallis sit amet. In finibus mi tortor, non hendrerit purus dictum at. Vivamus condimentum elementum neque et maximus. Cras auctor iaculis metus, at vulputate justo rhoncus eu. Aliquam laoreet mi euismod, lacinia est nec, euismod augue. In viverra tincidunt dolor vitae porttitor. Proin congue, mi eu laoreet congue, libero nunc porttitor tellus, non dictum magna erat sed purus. In nec luctus ligula, vel gravida urna. Sed lacinia mollis justo at hendrerit. Pellentesque gravida laoreet risus non auctor. Praesent ac sollicitudin eros. Vestibulum non viverra magna, sodales tristique ipsum. + +Etiam et lacinia eros, ut scelerisque turpis. Sed elit tortor, varius in nibh at, tempor euismod massa. Sed dapibus purus nec felis venenatis, nec rhoncus eros sagittis. Nullam elit orci, facilisis nec nunc sed, lobortis sagittis metus. Proin aliquam pharetra sagittis. Etiam ultrices nulla quis posuere elementum. Sed ultrices justo justo, eu tempor turpis rutrum vel. Pellentesque at cursus tellus. Vestibulum pulvinar tellus eget felis posuere bibendum. Etiam nec orci eleifend, gravida ipsum mollis, facilisis erat. In efficitur ac metus vel aliquam. Quisque arcu est, malesuada ut ligula ac, consectetur rutrum ex. Quisque varius viverra gravida. Suspendisse id leo quis felis imperdiet fringilla. Aenean ac accumsan urna. Fusce sapien mauris, varius pellentesque porta lobortis, tempus scelerisque metus. + +Vivamus tempus interdum felis, quis gravida ipsum auctor at. Cras sed ligula eu mauris semper pellentesque. Donec ut nibh at odio tempus euismod. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam ultrices odio ut faucibus mollis. Curabitur tincidunt accumsan luctus. Suspendisse tincidunt magna mi, et euismod nulla feugiat quis. Fusce finibus ac velit id fermentum. Aliquam venenatis egestas aliquam. Nulla diam libero, consectetur eu enim ac, dictum tempor lectus. Ut id tempus augue. Nulla facilisis, massa sit amet ultricies ultrices, eros lacus eleifend ex, vitae facilisis velit urna non leo. Ut libero ligula, venenatis ac odio id, posuere hendrerit eros. Cras eget sapien at mauris iaculis tincidunt. Nullam ut neque nisi. Aliquam hendrerit, magna non pellentesque iaculis, nulla libero molestie augue, non vehicula tortor sapien porttitor eros. + +Sed eget lectus nec enim porta gravida. Vestibulum id tincidunt nunc. Quisque scelerisque condimentum ipsum, eu accumsan orci facilisis non. Duis venenatis, est et mattis finibus, turpis urna rhoncus urna, gravida ultrices ipsum neque vitae erat. Vivamus massa enim, tristique vulputate faucibus a, luctus non dui. Vestibulum non risus lorem. Suspendisse eu orci accumsan, molestie enim nec, convallis augue. + +Nullam sed arcu turpis. Quisque ut elementum velit. Maecenas vitae sem vel eros vehicula hendrerit. Aenean suscipit convallis justo. Morbi in consectetur diam. Donec et efficitur justo, vestibulum convallis turpis. Proin sit amet enim id enim tempor tempus. Nam est augue, consectetur vitae ligula ut, tristique consequat nibh. + +Ut pharetra tortor auctor risus posuere, nec hendrerit nibh rhoncus. Aliquam tincidunt aliquam felis, at dignissim justo fermentum quis. Aenean malesuada, eros a ornare varius, sapien ex mollis nunc, vitae bibendum augue lectus vel mi. Nulla ultricies dui sed tellus sodales, at iaculis urna elementum. Sed et lacinia urna. Integer commodo nulla non quam rhoncus, bibendum tristique massa finibus. In posuere, lectus id tristique tristique, risus mauris consectetur erat, fringilla mattis metus eros in velit. Praesent est eros, tempor vel vulputate ut, luctus ut libero. Phasellus a urna porttitor, aliquam enim non, varius ipsum. Cras non ante ut ante egestas condimentum. Praesent finibus eleifend eros, tempor feugiat neque semper eget. Duis ullamcorper condimentum aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. + +Curabitur eget orci aliquet, pulvinar enim quis, hendrerit lectus. Suspendisse sed dictum lorem, nec porttitor ligula. Aliquam sit amet ligula sed lorem consectetur rhoncus ut a mauris. Quisque a ipsum sit amet augue mollis lobortis id nec risus. Phasellus vulputate justo in eros sodales vulputate. Fusce leo magna, condimentum quis vehicula id, malesuada at justo. Donec fringilla tortor in ullamcorper viverra. Vestibulum efficitur, quam ac consectetur dignissim, metus libero tincidunt libero, quis ultrices nisi mi eu erat. Nulla pharetra iaculis ullamcorper. Donec sit amet tortor congue risus elementum venenatis. Maecenas nisl nunc, imperdiet nec mattis sit amet, dignissim eget sapien. Nullam tristique turpis eu ante tempor, nec suscipit tortor sodales. Quisque cursus a orci quis molestie. Sed sit amet venenatis leo. Maecenas felis lacus, accumsan at accumsan non, ornare quis dolor. + +Aliquam vel enim eros. Curabitur sit amet risus ligula. Aliquam eget iaculis lacus, vitae efficitur nisl. Vestibulum convallis, risus at lacinia tempus, libero tortor rhoncus augue, id fringilla ipsum massa eget lorem. Maecenas justo leo, dignissim vitae finibus a, vehicula quis eros. Vivamus vel nisl porttitor, aliquam sapien et, semper risus. Praesent at sem tellus. Fusce sit amet fringilla elit. Nullam lorem sapien, vulputate eget lacus interdum, fermentum rutrum neque. Nulla scelerisque massa a felis cursus euismod. + +Mauris a mi posuere, eleifend velit in, luctus nisi. Donec mattis lacus velit, non laoreet odio posuere ac. Nulla efficitur fringilla orci a porta. Vestibulum est magna, fermentum id tempor eget, volutpat vel magna. Nunc non aliquam urna, ut congue urna. Aliquam purus nunc, pretium eget vehicula sed, vehicula sed sem. Quisque pellentesque velit sit amet orci dapibus tristique. Cras fringilla velit id ultrices scelerisque. Praesent porta egestas mauris, vitae accumsan quam. Aliquam molestie, magna sit amet maximus feugiat, arcu mauris ultricies lorem, id aliquam turpis arcu ac mauris. Etiam eu scelerisque neque. Donec sed quam vel est dictum convallis quis posuere tortor. Sed sit amet tortor eros. Sed odio purus, egestas at lacinia sed, consectetur id diam. Quisque tincidunt, ante eget mattis cursus, felis ante venenatis leo, in eleifend erat diam eu eros. + +Sed ac nunc mauris. Sed hendrerit ligula efficitur facilisis tincidunt. Morbi ut ornare lorem, sed facilisis ex. Aenean aliquam tristique mi, ac tristique metus rutrum eget. Curabitur id leo id massa commodo sagittis in at elit. Morbi viverra bibendum ligula vitae tristique. Etiam at est interdum, euismod nibh nec, condimentum arcu. Etiam orci ex, sagittis et tincidunt quis, finibus eu sapien. Sed urna lorem, suscipit a gravida vitae, sollicitudin vitae dolor. Cras ut imperdiet massa. Fusce ornare iaculis ipsum a cursus. Pellentesque vulputate, justo vel tincidunt egestas, lacus odio convallis odio, eu porttitor felis ipsum vitae libero. + +Phasellus eget nulla eget est convallis ornare ac non enim. Integer tincidunt massa eu tincidunt euismod. Proin at nulla in dui malesuada venenatis vitae at ligula. Curabitur dapibus mauris vitae turpis euismod, et pharetra quam molestie. Nulla id faucibus tortor. Pellentesque ultrices, turpis vel lobortis fermentum, sapien diam rhoncus sapien, quis tristique turpis lorem a mi. Nam bibendum, sem eget congue interdum, lacus orci convallis elit, ac porttitor lorem erat et orci. Integer elementum tortor et nisl posuere consectetur. Sed malesuada leo ac urna lacinia, malesuada luctus mauris faucibus. Duis consequat posuere lobortis. Cras interdum lacinia lacus. Cras ipsum sapien, porttitor eget pellentesque ut, aliquam ut magna. Integer luctus velit et elementum malesuada. Maecenas tempus mauris quis mollis posuere. + +Maecenas consequat urna elit, eget fringilla felis laoreet ac. In congue ullamcorper odio, sed malesuada turpis luctus sed. Nullam sodales interdum elit ac dignissim. Pellentesque placerat mollis velit, vulputate tristique neque aliquet vitae. Phasellus tempor viverra est, vitae vehicula justo imperdiet nec. Fusce dictum lacinia urna eget rhoncus. Nam imperdiet nisl orci, sed ultricies orci laoreet vel. In in pulvinar eros. Ut eu rhoncus eros. Suspendisse eu dui viverra, lacinia erat vitae, mattis metus. Aliquam vitae posuere sapien. Quisque eu lorem quis mi egestas varius. Aliquam erat volutpat. + +Duis eu arcu lobortis, vehicula orci ac, imperdiet dui. Etiam venenatis nisi quam, quis dapibus erat sagittis non. Suspendisse lacinia blandit interdum. Vivamus vitae sollicitudin leo. Vivamus sit amet commodo tellus, ac sagittis augue. Vivamus mi orci, ultricies ac nulla at, pharetra maximus diam. Nullam rhoncus volutpat magna eu auctor. + +Etiam commodo enim a leo pellentesque, in elementum odio lobortis. Vestibulum lobortis lobortis malesuada. Sed imperdiet ullamcorper viverra. Cras facilisis malesuada purus a consequat. Ut auctor neque mi, in scelerisque nibh ornare eu. In non dui in enim pretium ullamcorper non rutrum urna. Donec dictum porta orci sed malesuada. Morbi ac placerat felis. Aliquam lacus sem, ullamcorper sed laoreet ut, imperdiet non libero. Nunc non metus id justo accumsan ultricies. Donec in lacinia eros. Morbi non nunc diam. Aenean nisl massa, vestibulum vel fringilla vel, placerat eu leo. Sed feugiat malesuada ultricies. + +Nulla tristique massa eu tortor feugiat auctor. In viverra eu ex quis auctor. Praesent ac sapien orci. Proin sit amet orci posuere, ultricies orci in, fermentum justo. Vivamus tempus, ligula et aliquam egestas, enim orci pulvinar dui, in ultricies mauris turpis eu nisi. Pellentesque nec dui in ipsum laoreet convallis. Vestibulum a mi ornare, elementum ex sed, rhoncus neque. + +Nullam sed orci id sem tincidunt suscipit. Cras ac leo at magna facilisis blandit. Aliquam vitae tristique nisi. Nulla vestibulum felis pretium lectus commodo, id eleifend risus eleifend. Mauris bibendum facilisis est vitae scelerisque. Cras interdum dapibus ligula, nec porta lacus imperdiet at. Cras rhoncus bibendum lorem, congue mollis libero dignissim eget. Sed gravida, mauris sit amet sodales posuere, tellus felis imperdiet arcu, quis iaculis orci orci vitae ipsum. Curabitur id orci odio. Aliquam vitae rutrum lacus. + +Maecenas tristique est felis, eget posuere lorem dapibus non. Maecenas eu interdum lectus. Nullam placerat sit amet quam non hendrerit. Nulla facilisis ornare mollis. Cras sed metus facilisis, mattis dui in, auctor est. Morbi vehicula venenatis est, vitae egestas felis tincidunt vitae. Donec iaculis massa id justo ornare rhoncus. Nulla tempor felis ex, eu consectetur justo dictum sed. Integer eget laoreet nibh. Duis vitae pellentesque tellus, id blandit justo. Cras quis mollis eros. Quisque rhoncus dignissim enim at sollicitudin. Vestibulum sit amet diam sed quam sodales finibus. Nulla maximus orci sit amet porttitor vestibulum. Nam consequat urna at varius vulputate. + +Suspendisse laoreet luctus mauris. Aenean mollis felis urna, ac pretium nunc rutrum non. Aenean semper, risus vitae iaculis eleifend, odio ligula tempus nunc, sit amet suscipit nibh lorem quis ipsum. Nam et placerat sapien. In luctus accumsan risus, id pulvinar elit venenatis eu. Phasellus elementum leo urna, a dapibus neque facilisis eget. Sed sit amet tempor tortor. Fusce iaculis, ipsum nec faucibus scelerisque, felis tortor condimentum purus, in fringilla ex est ac nisl. Nunc et interdum diam. Donec venenatis, dolor quis dignissim euismod, libero ante elementum libero, vitae laoreet purus velit sit amet ipsum. Quisque urna tellus, imperdiet eget gravida a, fringilla commodo diam. + +In posuere nisi dictum tortor elementum iaculis. In dignissim diam sit amet volutpat elementum. Curabitur ultricies mi libero, sit amet feugiat massa viverra sed. Vivamus justo nibh, commodo nec elementum eu, suscipit vel elit. Fusce ac cursus libero, et volutpat augue. In tempus ultricies libero, ac rutrum mauris. Morbi vestibulum tellus eu dui dignissim euismod. In hac habitasse platea dictumst. + +Nam rhoncus hendrerit ex et mattis. Sed varius, arcu quis placerat viverra, ex lorem ultrices arcu, nec fringilla ipsum metus ut ligula. Sed in luctus mi. Pellentesque sed eros nisl. Praesent rutrum magna metus, vel efficitur eros lobortis efficitur. Mauris vestibulum urna at ligula lobortis sollicitudin. Aenean rhoncus auctor leo vel interdum. Cras sollicitudin massa leo, et eleifend metus scelerisque sit amet. Praesent dapibus euismod libero a fermentum. + +Curabitur cursus lacus sit amet est feugiat euismod. Nulla accumsan risus congue lorem facilisis scelerisque. In sed lectus elementum, porttitor nisl vulputate, pulvinar mi. Maecenas eu mollis odio. In faucibus sagittis magna, vitae mollis nisi iaculis non. Aenean dictum lectus ac arcu vehicula fringilla. Curabitur accumsan efficitur libero, eget consequat magna ultrices vel. Donec fermentum vel orci eget finibus. Etiam in massa ante. Mauris gravida enim lacus, sit amet accumsan massa suscipit a. Mauris id bibendum ex, et convallis nisl. Morbi luctus, orci in malesuada finibus, neque turpis convallis justo, id gravida sem purus eget turpis. Fusce eu laoreet justo. + +Nulla facilisi. Nulla varius risus quam, sit amet aliquam felis lacinia a. Ut sapien felis, tincidunt in pellentesque sit amet, vehicula id elit. Morbi a congue nunc. Sed justo neque, rutrum tincidunt hendrerit in, luctus at mauris. Suspendisse porttitor ex vitae felis luctus, et bibendum eros consequat. Maecenas vitae lectus eget est volutpat fermentum ac ac elit. In hac habitasse platea dictumst. Aenean luctus ex nec orci euismod aliquam. Integer a dolor elementum, mollis magna vitae, porta dui. In ac erat posuere, facilisis lacus eget, venenatis velit. Nulla ut sapien tincidunt, ultricies lectus aliquet, varius odio. Curabitur viverra congue ipsum, non finibus enim lobortis vitae. + +Duis vestibulum maximus est, sollicitudin dapibus sapien accumsan sit amet. Cras luctus, massa malesuada ornare imperdiet, dolor lorem blandit est, a blandit erat quam vel tortor. Vestibulum id semper ipsum. Ut nec ante eget velit fringilla sagittis. Duis sit amet lobortis nisi. Aenean interdum dui ut metus suscipit, a pretium tortor ultrices. Nullam tincidunt bibendum nisl, vitae tincidunt urna tincidunt tempus. Donec vitae porta sem. Phasellus venenatis egestas ligula, quis volutpat ipsum lacinia et. Pellentesque placerat ipsum elit, a feugiat libero scelerisque fringilla. Suspendisse ullamcorper congue nisi ut fringilla. Aliquam quis suscipit orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam at nulla volutpat, viverra sem ac, blandit erat. Quisque gravida quam eu magna hendrerit, quis varius diam scelerisque. Suspendisse nisi enim, dignissim sit amet tristique ut, tempus sodales nibh. + +Nulla ac eleifend leo. Quisque laoreet finibus mattis. Nam est nulla, maximus sed luctus non, volutpat quis ipsum. Praesent et quam dolor. Nulla facilisi. Aliquam enim orci, volutpat vitae mattis pellentesque, fringilla ut eros. Fusce semper arcu et sapien rutrum laoreet. Maecenas vel imperdiet lectus. Mauris et pulvinar justo. Mauris sed luctus diam. Fusce a odio eget ante consequat volutpat. Nullam at lorem ut dui ornare aliquam nec non justo. Mauris turpis eros, blandit eget elementum ullamcorper, molestie vel quam. Aenean pretium interdum ligula ac fringilla. Aenean et felis lorem. Ut id lectus quis risus finibus condimentum. + +Curabitur aliquet quis justo sed posuere. Donec eu libero eget mi ullamcorper placerat. Etiam massa mi, lobortis eget fermentum in, facilisis vel lectus. In hac habitasse platea dictumst. Nam laoreet sodales metus, nec finibus nulla volutpat bibendum. Aenean ut vulputate dolor, vitae venenatis est. Fusce ipsum libero, laoreet sit amet justo at, auctor tristique arcu. Praesent pellentesque efficitur velit non accumsan. Proin fermentum tempus ante, at eleifend lectus fringilla sit amet. Nunc et diam ac velit tristique malesuada a id mauris. Sed euismod turpis lacus, a maximus dolor semper in. Nulla mauris tortor, dignissim vel mauris sed, efficitur ultrices nulla. Donec sed molestie libero. Quisque nec congue eros, ut consequat lectus. Suspendisse vitae tortor sapien. Sed vitae pellentesque dolor. + +Suspendisse dictum velit metus, vel mollis erat imperdiet ut. Mauris et sapien eleifend, malesuada ante vehicula, ornare tellus. Integer condimentum mattis risus, nec luctus lacus convallis a. Phasellus bibendum consequat nisi, ut consequat dui bibendum a. Sed venenatis lobortis turpis, a venenatis ex sodales eu. Duis sit amet sem dui. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae tellus a ex tristique mattis. Quisque viverra vitae turpis accumsan imperdiet. Cras nunc erat, commodo et malesuada at, vulputate in lorem. Fusce tempor venenatis dui consequat auctor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. + +Fusce et ligula sem. In condimentum dolor metus, eu luctus justo ultrices nec. Aenean vitae ligula non erat tincidunt blandit. Pellentesque luctus tellus ante, mollis cursus arcu posuere a. Ut sed lacus suscipit, dapibus quam non, elementum purus. Nullam sed venenatis tortor, id luctus lacus. Suspendisse orci felis, porta et sagittis at, posuere vitae lectus. + +Cras in ligula ut mi viverra efficitur at sed erat. Donec congue suscipit orci, eu volutpat augue ultricies non. Maecenas imperdiet tincidunt commodo. Cras quis vulputate urna, et malesuada lorem. Donec id convallis nibh, non congue lacus. Curabitur et scelerisque nisl. Maecenas vestibulum elit ipsum, in posuere tortor placerat sit amet. Nullam nec ex eget libero mattis commodo a at leo. Vestibulum aliquet, eros quis facilisis aliquam, mi arcu aliquet nisl, in tempus massa sapien at tortor. + +Maecenas et dolor sed sapien lacinia fringilla eget et nibh. Aenean viverra urna sit amet lobortis vestibulum. Aliquam vehicula rutrum magna ut aliquam. Maecenas pharetra volutpat porttitor. In id ultricies sapien, a accumsan lectus. Fusce in elit a ex auctor rutrum sit amet ac lorem. Mauris eu mi a nisl vehicula mattis. Donec dictum velit nec libero bibendum, in volutpat metus viverra. Vivamus eget sollicitudin nunc, ac vestibulum erat. Sed dolor risus, semper nec lorem vitae, vehicula molestie purus. Quisque ac lectus iaculis, ultrices leo sit amet, mattis erat. Curabitur lorem mauris, vestibulum vel risus eu, molestie facilisis elit. Pellentesque habitant morbi tristique senectus et netus et volutpat. diff --git a/test/load/coffee/loadTest.coffee b/test/load/coffee/loadTest.coffee new file mode 100644 index 00000000..26a23fba --- /dev/null +++ b/test/load/coffee/loadTest.coffee @@ -0,0 +1,71 @@ +request = require "request" +Settings = require "settings-sharelatex" +async = require("async") +fs = require("fs") +_ = require("underscore") +concurentCompiles = 5 +totalCompiles = 50 + +buildUrl = (path) -> "http://#{Settings.internal.clsi.host}:#{Settings.internal.clsi.port}/#{path}" + +mainTexContent = fs.readFileSync("./bulk.tex", "utf-8") + +compileTimes = [] +failedCount = 0 + +getAverageCompileTime = -> + totalTime = _.reduce compileTimes, (sum, time)-> + sum + time + , 0 + return totalTime / compileTimes.length + +makeRequest = (compileNumber, callback)-> + bulkBodyCount = 7 + bodyContent = "" + while --bulkBodyCount + bodyContent = bodyContent+=mainTexContent + + + startTime = new Date() + request.post { + url: buildUrl("project/loadcompile-#{compileNumber}/compile") + json: + compile: + resources: [ + path: "main.tex" + content: """ + \\documentclass{article} + \\begin{document} + #{bodyContent} + \\end{document} + """ + ] + }, (err, response, body)-> + if response.statusCode != 200 + failedCount++ + return callback("compile #{compileNumber} failed") + if err? + failedCount++ + return callback("failed") + totalTime = new Date() - startTime + console.log totalTime+"ms" + compileTimes.push(totalTime) + callback(err) + + +jobs = _.map [1..totalCompiles], (i)-> + return (cb)-> + makeRequest(i, cb) + +startTime = new Date() +async.parallelLimit jobs, concurentCompiles, (err)-> + if err? + console.error err + console.log("total time taken = #{(new Date() - startTime)/1000}s") + console.log("total compiles = #{totalCompiles}") + console.log("concurent compiles = #{concurentCompiles}") + console.log("average time = #{getAverageCompileTime()/1000}s") + console.log("max time = #{_.max(compileTimes)/1000}s") + console.log("min time = #{_.min(compileTimes)/1000}s") + console.log("total failures = #{failedCount}") + From c2054a5ec334b3726a7405f762e73ac8e0310ed0 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 14 Aug 2015 14:47:42 +0100 Subject: [PATCH 058/709] add memory logger from metrics-sharelatex --- app.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app.coffee b/app.coffee index 9083bb4b..0c1248a5 100644 --- a/app.coffee +++ b/app.coffee @@ -10,6 +10,7 @@ fs = require "fs" Metrics = require "metrics-sharelatex" Metrics.initialize("clsi") Metrics.open_sockets.monitor(logger) +Metrics.memory.monitor(logger) ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager" OutputCacheManager = require "./app/js/OutputCacheManager" From caef254b80c05d75e4ce7fa7935eb5116cd743de Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 8 Sep 2015 10:19:46 -0300 Subject: [PATCH 059/709] move texcount to docker --- app/coffee/CompileManager.coffee | 19 ++++++++-------- test/unit/coffee/CompileManagerTests.coffee | 25 ++++++++++++++------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index f689ae88..0392ccef 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -7,6 +7,8 @@ Path = require "path" logger = require "logger-sharelatex" Metrics = require "./Metrics" child_process = require "child_process" +CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner") +fs = require("fs") module.exports = CompileManager = doCompile: (request, callback = (error, outputFiles) ->) -> @@ -142,15 +144,12 @@ module.exports = CompileManager = return results wordcount: (project_id, file_name, callback = (error, pdfPositions) ->) -> - base_dir = Settings.path.synctexBaseDir(project_id) - file_path = base_dir + "/" + file_name - CompileManager._runWordcount [file_path], (error, stdout) -> - return callback(error) if error? - logger.log project_id: project_id, file_name: file_name, stdout: stdout, "wordcount output" - callback null, CompileManager._parseWordcountFromOutput(stdout) + file_path = "$COMPILE_DIR/" + file_name + command = [ "texcount", file_path, "-out=" + file_path + ".wc"] + directory = Path.join(Settings.path.compilesDir, project_id) + timeout = 10 * 1000 - _runWordcount: (args, callback = (error, stdout) ->) -> - seconds = 1000 - child_process.execFile "texcount", args, timeout: 10 * seconds, (error, stdout, stderr) -> + CommandRunner.run project_id, command, directory, timeout, (error) -> return callback(error) if error? - callback(null, stdout) \ No newline at end of file + stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") + callback null, CompileManager._parseWordcountFromOutput(stdout) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index eae49627..10ab2eac 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -16,6 +16,8 @@ describe "CompileManager", -> "settings-sharelatex": @Settings = { path: compilesDir: "/compiles/dir" } "logger-sharelatex": @logger = { log: sinon.stub() } "child_process": @child_process = {} + "./CommandRunner": @CommandRunner = {} + "fs": @fs = {} @callback = sinon.stub() describe "doCompile", -> @@ -175,16 +177,24 @@ describe "CompileManager", -> describe "wordcount", -> beforeEach -> + @CommandRunner.run = sinon.stub().callsArg(4) + @fs.readFileSync = sinon.stub().returns @stdout = "Encoding: ascii\nWords in text: 2" + @callback = sinon.stub() + + @project_id = "project-id-123" + @timeout = 10 * 1000 @file_name = "main.tex" - @child_process.execFile = sinon.stub() - @child_process.execFile.callsArgWith(3, null, @stdout = "Encoding: ascii\nWords in text: 2", "") - @Settings.path.synctexBaseDir = (project_id) => "#{@Settings.path.compilesDir}/#{@project_id}" + @Settings.path.compilesDir = "/local/compile/directory" + @CompileManager.wordcount @project_id, @file_name, @callback - it "should execute the texcount binary", -> - file_path = "#{@Settings.path.compilesDir}/#{@project_id}/#{@file_name}" - @child_process.execFile - .calledWith("texcount", [file_path], timeout: 10000) + it "should run the texcount command", -> + @directory = "#{@Settings.path.compilesDir}/#{@project_id}" + @file_path = "$COMPILE_DIR/#{@file_name}" + @command =[ "texcount", @file_path, "-out=" + @file_path + ".wc"] + + @CommandRunner.run + .calledWith(@project_id, @command, @directory, @timeout) .should.equal true it "should call the callback with the parsed output", -> @@ -200,4 +210,3 @@ describe "CompileManager", -> mathDisplay: 0 }) .should.equal true - From 6fae6ff40c91f22fcbf9a45dd0a40084cada05cf Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 8 Sep 2015 21:58:27 +0100 Subject: [PATCH 060/709] don't error if a http resource can not be download try and continue, log the error but you might still be able to compile. prevents issue with badly uploaded images in filstore --- app/coffee/ResourceWriter.coffee | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 78667013..faeaf119 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -5,6 +5,7 @@ async = require "async" mkdirp = require "mkdirp" OutputFileFinder = require "./OutputFileFinder" Metrics = require "./Metrics" +logger = require "logger-sharelatex" module.exports = ResourceWriter = syncResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @@ -55,13 +56,10 @@ module.exports = ResourceWriter = return callback(error) if error? # TODO: Don't overwrite file if it hasn't been modified if resource.url? - UrlCache.downloadUrlToFile( - project_id, - resource.url, - path, - resource.modified, - callback - ) + UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)-> + if err? + logger.err err:err, "error downloading file for resources" + callback() #try and continue compiling even if http resource can not be downloaded at this time else fs.writeFile path, resource.content, callback From 440d1c605ff5fbd1e962782e801efbd53d4b8491 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 9 Sep 2015 09:44:38 +0100 Subject: [PATCH 061/709] added test to check compile should continue on error downloading http resource also improved logging --- app/coffee/ResourceWriter.coffee | 2 +- test/unit/coffee/ResourceWriterTests.coffee | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index faeaf119..25b2625f 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -58,7 +58,7 @@ module.exports = ResourceWriter = if resource.url? UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)-> if err? - logger.err err:err, "error downloading file for resources" + logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources" callback() #try and continue compiling even if http resource can not be downloaded at this time else fs.writeFile path, resource.content, callback diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 1e7b3e77..3e87b5ca 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -1,6 +1,6 @@ SandboxedModule = require('sandboxed-module') sinon = require('sinon') -require('chai').should() +should = require('chai').should() modulePath = require('path').join __dirname, '../../../app/js/ResourceWriter' path = require "path" @@ -94,7 +94,7 @@ describe "ResourceWriter", -> path: "main.tex" url: "http://www.example.com/main.tex" modified: Date.now() - @UrlCache.downloadUrlToFile = sinon.stub().callsArg(4) + @UrlCache.downloadUrlToFile = sinon.stub().callsArgWith(4, "fake error downloading file") @ResourceWriter._writeResourceToDisk(@project_id, @resource, @basePath, @callback) it "should ensure the directory exists", -> @@ -110,6 +110,9 @@ describe "ResourceWriter", -> it "should call the callback", -> @callback.called.should.equal true + it "should not return an error if the resource writer errored", -> + should.not.exist @callback.args[0][0] + describe "with a content based resource", -> beforeEach -> @resource = From 561eaa0d66e2af6296fa475d010d6f3f35f6d8f7 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 9 Sep 2015 12:43:32 +0100 Subject: [PATCH 062/709] add -inc to word count use -inc to word count included files also moved private function to bottom --- app/coffee/CompileController.coffee | 2 +- app/coffee/CompileManager.coffee | 24 ++++++++++----------- test/unit/coffee/CompileManagerTests.coffee | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index ce107f8e..f7d46b13 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -68,7 +68,7 @@ module.exports = CompileController = } wordcount: (req, res, next = (error) ->) -> - file = req.query.file + file = req.query.file || "main.tex" project_id = req.params.project_id CompileManager.wordcount project_id, file, (error, result) -> diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 0392ccef..b4a8357b 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -111,6 +111,18 @@ module.exports = CompileManager = } return results + wordcount: (project_id, file_name, callback = (error, pdfPositions) ->) -> + logger.log project_id:project_id, file_name:file_name, "running wordcount" + file_path = "$COMPILE_DIR/" + file_name + command = [ "texcount", '-inc', file_path, "-out=" + file_path + ".wc"] + directory = Path.join(Settings.path.compilesDir, project_id) + timeout = 10 * 1000 + + CommandRunner.run project_id, command, directory, timeout, (error) -> + return callback(error) if error? + stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") + callback null, CompileManager._parseWordcountFromOutput(stdout) + _parseWordcountFromOutput: (output) -> results = { encode: "" @@ -140,16 +152,4 @@ module.exports = CompileManager = results['mathInline'] = parseInt(info, 10) if data.indexOf("displayed") > -1 results['mathDisplay'] = parseInt(info, 10) - return results - - wordcount: (project_id, file_name, callback = (error, pdfPositions) ->) -> - file_path = "$COMPILE_DIR/" + file_name - command = [ "texcount", file_path, "-out=" + file_path + ".wc"] - directory = Path.join(Settings.path.compilesDir, project_id) - timeout = 10 * 1000 - - CommandRunner.run project_id, command, directory, timeout, (error) -> - return callback(error) if error? - stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") - callback null, CompileManager._parseWordcountFromOutput(stdout) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 10ab2eac..d6678b94 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -191,7 +191,7 @@ describe "CompileManager", -> it "should run the texcount command", -> @directory = "#{@Settings.path.compilesDir}/#{@project_id}" @file_path = "$COMPILE_DIR/#{@file_name}" - @command =[ "texcount", @file_path, "-out=" + @file_path + ".wc"] + @command =[ "texcount", '-inc', @file_path, "-out=" + @file_path + ".wc"] @CommandRunner.run .calledWith(@project_id, @command, @directory, @timeout) From 74c393cda3130aed5259ff5cc2d64ef728aed5ce Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 9 Sep 2015 13:52:45 +0100 Subject: [PATCH 063/709] - fixed bug with texcount returning wrong data for nauty lines - improved acceptence test for word count to use nauty lines --- app/coffee/CompileManager.coffee | 6 +- test/acceptance/coffee/SynctexTests.coffee | 18 - test/acceptance/coffee/WordcountTests.coffee | 34 + test/acceptance/fixtures/naugty_strings.txt | 626 +++++++++++++++++++ test/unit/coffee/CompileManagerTests.coffee | 2 +- 5 files changed, 664 insertions(+), 22 deletions(-) create mode 100644 test/acceptance/coffee/WordcountTests.coffee create mode 100644 test/acceptance/fixtures/naugty_strings.txt diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index b4a8357b..296522f6 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -146,10 +146,10 @@ module.exports = CompileManager = results['outside'] = parseInt(info, 10) if data.indexOf("of head") > -1 results['headers'] = parseInt(info, 10) - if data.indexOf("float") > -1 + if data.indexOf("Number of floats/tables/figures") > -1 results['elements'] = parseInt(info, 10) - if data.indexOf("inlines") > -1 + if data.indexOf("Number of math inlines") > -1 results['mathInline'] = parseInt(info, 10) - if data.indexOf("displayed") > -1 + if data.indexOf("Number of math displayed") > -1 results['mathDisplay'] = parseInt(info, 10) return results diff --git a/test/acceptance/coffee/SynctexTests.coffee b/test/acceptance/coffee/SynctexTests.coffee index 8979b983..02b23978 100644 --- a/test/acceptance/coffee/SynctexTests.coffee +++ b/test/acceptance/coffee/SynctexTests.coffee @@ -35,21 +35,3 @@ describe "Syncing", -> code: [ { file: 'main.tex', line: 3, column: -1 } ] ) done() - - describe "wordcount file", -> - it "should return wordcount info", (done) -> - Client.wordcount @project_id, "main.tex", (error, result) -> - throw error if error? - expect(result).to.deep.equal( - texcount: { - encode: "ascii" - textWords: 2 - headWords: 0 - outside: 0 - headers: 0 - elements: 0 - mathInline: 0 - mathDisplay: 0 - } - ) - done() diff --git a/test/acceptance/coffee/WordcountTests.coffee b/test/acceptance/coffee/WordcountTests.coffee new file mode 100644 index 00000000..1789cfcd --- /dev/null +++ b/test/acceptance/coffee/WordcountTests.coffee @@ -0,0 +1,34 @@ +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +expect = require("chai").expect +path = require("path") +fs = require("fs") + +describe "Syncing", -> + before (done) -> + @request = + resources: [ + path: "main.tex" + content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") + ] + @project_id = Client.randomId() + Client.compile @project_id, @request, (@error, @res, @body) => done() + + describe "wordcount file", -> + it "should return wordcount info", (done) -> + Client.wordcount @project_id, "main.tex", (error, result) -> + throw error if error? + expect(result).to.deep.equal( + texcount: { + encode: "utf8" + textWords: 2281 + headWords: 2 + outside: 0 + headers: 2 + elements: 0 + mathInline: 6 + mathDisplay: 0 + } + ) + done() diff --git a/test/acceptance/fixtures/naugty_strings.txt b/test/acceptance/fixtures/naugty_strings.txt new file mode 100644 index 00000000..92eb1ddc --- /dev/null +++ b/test/acceptance/fixtures/naugty_strings.txt @@ -0,0 +1,626 @@ +\documentclass{article} +\usepackage[utf8]{inputenc} + +\title{eee} +\author{henry.oswald } +\date{September 2015} + +\usepackage{natbib} +\usepackage{graphicx} + +\begin{document} + +\maketitle + +\section{Introduction} + +Encoding: utf8 + +# Reserved Strings +# +# Strings which may be used elsewhere in code + +undefined +undef +null +NULL +(null) +nil +NIL +true +false +True +False +None +\ +\\ + +# Numeric Strings +# +# Strings which can be interpreted as numeric + +0 +1 +1.00 +$1.00 +1/2 +1E2 +1E02 +1E+02 +-1 +-1.00 +-$1.00 +-1/2 +-1E2 +-1E02 +-1E+02 +1/0 +0/0 +-2147483648/-1 +-9223372036854775808/-1 +0.00 +0..0 +. +0.0.0 +0,00 +0,,0 +, +0,0,0 +0.0/0 +1.0/0.0 +0.0/0.0 +1,0/0,0 +0,0/0,0 +--1 +- +-. +-, +999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +NaN +Infinity +-Infinity +0x0 +0xffffffff +0xffffffffffffffff +0xabad1dea +123456789012345678901234567890123456789 +1,000.00 +1 000.00 +1'000.00 +1,000,000.00 +1 000 000.00 +1'000'000.00 +1.000,00 +1 000,00 +1'000,00 +1.000.000,00 +1 000 000,00 +1'000'000,00 +01000 +08 +09 +2.2250738585072011e-308 + +# Special Characters +# +# Strings which contain common special ASCII characters (may need to be escaped) + +,./;'[]\-= +<>?:"{}|_+ +!@#$%^&*()`~ + +# Unicode Symbols +# +# Strings which contain common unicode symbols (e.g. smart quotes) + +Ω≈ç√∫˜µ≤≥÷ +åß∂ƒ©˙∆˚¬…æ +œ∑´®†¥¨ˆøπ“‘ +¡™£¢∞§¶•ªº–≠ +¸˛Ç◊ı˜Â¯˘¿ +ÅÍÎÏ˝ÓÔÒÚÆ☃ +Œ„´‰ˇÁ¨ˆØ∏”’ +`⁄€‹›fifl‡°·‚—± +⅛⅜⅝⅞ +ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя +٠١٢٣٤٥٦٧٨٩ + +# Unicode Subscript/Superscript +# +# Strings which contain unicode subscripts/superscripts; can cause rendering issues + +⁰⁴⁵ +₀₁₂ +⁰⁴⁵₀₁₂ + +# Quotation Marks +# +# Strings which contain misplaced quotation marks; can cause encoding errors + +' +" +'' +"" +'"' +"''''"'" +"'"'"''''" + +# Two-Byte Characters +# +# Strings which contain two-byte characters: can cause rendering issues or character-length issues + +田中さんにあげて下さい +パーティーへ行かないか +和製漢語 +部落格 +사회과학원 어학연구소 +찦차를 타고 온 펲시맨과 쑛다리 똠방각하 +社會科學院語學研究所 +울란바토르 +𠜎𠜱𠝹𠱓𠱸𠲖𠳏 + +# Japanese Emoticons +# +# Strings which consists of Japanese-style emoticons which are popular on the web + +ヽ༼ຈل͜ຈ༽ノ ヽ༼ຈل͜ຈ༽ノ +(。◕ ∀ ◕。) +`ィ(´∀`∩ +__ロ(,_,*) +・( ̄∀ ̄)・:*: +゚・✿ヾ╲(。◕‿◕。)╱✿・゚ +,。・:*:・゜’( ☻ ω ☻ )。・:*:・゜’ +(╯°□°)╯︵ ┻━┻) +(ノಥ益ಥ)ノ ┻━┻ +( ͡° ͜ʖ ͡°) + +# Emoji +# +# Strings which contain Emoji; should be the same behavior as two-byte characters, but not always + +😍 +👩🏽 +👾 🙇 💁 🙅 🙆 🙋 🙎 🙍 +🐵 🙈 🙉 🙊 +❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙 +✋🏿 💪🏿 👐🏿 🙌🏿 👏🏿 🙏🏿 +🚾 🆒 🆓 🆕 🆖 🆗 🆙 🏧 +0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 + +# Unicode Numbers +# +# Strings which contain unicode numbers; if the code is localized, it should see the input as numeric + +123 +١٢٣ + +# Right-To-Left Strings +# +# Strings which contain text that should be rendered RTL if possible (e.g. Arabic, Hebrew) + +ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو. +בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ +הָיְתָהtestالصفحات التّحول +﷽ +ﷺ + +# Unicode Spaces +# +# Strings which contain unicode space characters with special properties (c.f. https://www.cs.tut.fi/~jkorpela/chars/spaces.html) + +​ +  +᠎ +  + +␣ +␢ +␡ + +# Trick Unicode +# +# Strings which contain unicode with unusual properties (e.g. Right-to-left override) (c.f. http://www.unicode.org/charts/PDF/U2000.pdf) + +‪‪test‪ +‫test‫ +
test
 +test⁠test‫ +⁦test⁧ + +# Zalgo Text +# +# Strings which contain "corrupted" text. The corruption will not appear in non-HTML text, however. (via http://www.eeemo.net) + +Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣ +̡͓̞ͅI̗̘̦͝n͇͇͙v̮̫ok̲̫̙͈i̖͙̭̹̠̞n̡̻̮̣̺g̲͈͙̭͙̬͎ ̰t͔̦h̞̲e̢̤ ͍̬̲͖f̴̘͕̣è͖ẹ̥̩l͖͔͚i͓͚̦͠n͖͍̗͓̳̮g͍ ̨o͚̪͡f̘̣̬ ̖̘͖̟͙̮c҉͔̫͖͓͇͖ͅh̵̤̣͚͔á̗̼͕ͅo̼̣̥s̱͈̺̖̦̻͢.̛̖̞̠̫̰ +̗̺͖̹̯͓Ṯ̤͍̥͇͈h̲́e͏͓̼̗̙̼̣͔ ͇̜̱̠͓͍ͅN͕͠e̗̱z̘̝̜̺͙p̤̺̹͍̯͚e̠̻̠͜r̨̤͍̺̖͔̖̖d̠̟̭̬̝͟i̦͖̩͓͔̤a̠̗̬͉̙n͚͜ ̻̞̰͚ͅh̵͉i̳̞v̢͇ḙ͎͟-҉̭̩̼͔m̤̭̫i͕͇̝̦n̗͙ḍ̟ ̯̲͕͞ǫ̟̯̰̲͙̻̝f ̪̰̰̗̖̭̘͘c̦͍̲̞͍̩̙ḥ͚a̮͎̟̙͜ơ̩̹͎s̤.̝̝ ҉Z̡̖̜͖̰̣͉̜a͖̰͙̬͡l̲̫̳͍̩g̡̟̼̱͚̞̬ͅo̗͜.̟ +̦H̬̤̗̤͝e͜ ̜̥̝̻͍̟́w̕h̖̯͓o̝͙̖͎̱̮ ҉̺̙̞̟͈W̷̼̭a̺̪͍į͈͕̭͙̯̜t̶̼̮s̘͙͖̕ ̠̫̠B̻͍͙͉̳ͅe̵h̵̬͇̫͙i̹͓̳̳̮͎̫̕n͟d̴̪̜̖ ̰͉̩͇͙̲͞ͅT͖̼͓̪͢h͏͓̮̻e̬̝̟ͅ ̤̹̝W͙̞̝͔͇͝ͅa͏͓͔̹̼̣l̴͔̰̤̟͔ḽ̫.͕ +Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮ + +# Unicode Upsidedown +# +# Strings which contain unicode with an "upsidedown" effect (via http://www.upsidedowntext.com) + +˙ɐnbᴉlɐ ɐuƃɐɯ ǝɹolop ʇǝ ǝɹoqɐl ʇn ʇunpᴉpᴉɔuᴉ ɹodɯǝʇ poɯsnᴉǝ op pǝs 'ʇᴉlǝ ƃuᴉɔsᴉdᴉpɐ ɹnʇǝʇɔǝsuoɔ 'ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥ +00˙Ɩ$- + +# Unicode font +# +# Strings which contain bold/italic/etc. versions of normal characters + +The quick brown fox jumps over the lazy dog +𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠 +𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌 +𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈 +𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰 +𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘 +𝚃𝚑𝚎 𝚚𝚞𝚒𝚌𝚔 𝚋𝚛𝚘𝚠𝚗 𝚏𝚘𝚡 𝚓𝚞𝚖𝚙𝚜 𝚘𝚟𝚎𝚛 𝚝𝚑𝚎 𝚕𝚊𝚣𝚢 𝚍𝚘𝚐 +⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢ + +# Script Injection +# +# Strings which attempt to invoke a benign script injection; shows vulnerability to XSS + + +<script>alert('123');</script> + + +"> +'> +> + +< / script >< script >alert(123)< / script > + onfocus=JaVaSCript:alert(123) autofocus +" onfocus=JaVaSCript:alert(123) autofocus +' onfocus=JaVaSCript:alert(123) autofocus +<script>alert(123)</script> +ript>alert(123)ript> +--> +";alert(123);t=" +';alert(123);t=' +JavaSCript:alert(123) +;alert(123); +src=JaVaSCript:prompt(132) +">javascript:alert(1); +javascript:alert(1); +javascript:alert(1); +javascript:alert(1); +javascript:alert(1); +javascript:alert(1); +javascript:alert(1); +'`"><\x3Cscript>javascript:alert(1) +'`"><\x00script>javascript:alert(1) +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +ABC
DEF +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +XXX + + + +<a href=http://foo.bar/#x=`y></a><img alt="`><img src=x:x onerror=javascript:alert(1)></a>"> +<!--[if]><script>javascript:alert(1)</script --> +<!--[if<img src=x onerror=javascript:alert(1)//]> --> +<script src="/\%(jscript)s"></script> +<script src="\\%(jscript)s"></script> +<IMG """><SCRIPT>alert("XSS")</SCRIPT>"> +<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))> +<IMG SRC=# onmouseover="alert('xxs')"> +<IMG SRC= onmouseover="alert('xxs')"> +<IMG onmouseover="alert('xxs')"> +<IMG SRC=javascript:alert('XSS')> +<IMG SRC=javascript:alert('XSS')> +<IMG SRC=javascript:alert('XSS')> +<IMG SRC="jav ascript:alert('XSS');"> +<IMG SRC="jav ascript:alert('XSS');"> +<IMG SRC="jav ascript:alert('XSS');"> +<IMG SRC="jav ascript:alert('XSS');"> +perl -e 'print "<IMG SRC=java\0script:alert(\"XSS\")>";' > out +<IMG SRC="  javascript:alert('XSS');"> +<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT> +<BODY onload!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")> +<SCRIPT/SRC="http://ha.ckers.org/xss.js"></SCRIPT> +<<SCRIPT>alert("XSS");//<</SCRIPT> +<SCRIPT SRC=http://ha.ckers.org/xss.js?< B > +<SCRIPT SRC=//ha.ckers.org/.j> +<IMG SRC="javascript:alert('XSS')" +<iframe src=http://ha.ckers.org/scriptlet.html < +\";alert('XSS');// +<plaintext> + +# SQL Injection +# +# Strings which can cause a SQL injection if inputs are not sanitized + +1;DROP TABLE users +1'; DROP TABLE users-- 1 +' OR 1=1 -- 1 +' OR '1'='1 + +# Server Code Injection +# +# Strings which can cause user to run code on server as a privileged user (c.f. https://news.ycombinator.com/item?id=7665153) + +- +-- +--version +--help +$USER +/dev/null; touch /tmp/blns.fail ; echo +`touch /tmp/blns.fail` +$(touch /tmp/blns.fail) +@{[system "touch /tmp/blns.fail"]} + +# Command Injection (Ruby) +# +# Strings which can call system commands within Ruby/Rails applications + +eval("puts 'hello world'") +System("ls -al /") +`ls -al /` +Kernel.exec("ls -al /") +Kernel.exit(1) +%x('ls -al /') + +# XXE Injection (XML) +# +# String which can reveal system files when parsed by a badly configured XML parser + +<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo> + +# Unwanted Interpolation +# +# Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string. + +$HOME +$ENV{'HOME'} +%d +%s +%*.*s + +# File Inclusion +# +# Strings which can cause user to pull in files that should not be a part of a web server + +../../../../../../../../../../../etc/passwd%00 +../../../../../../../../../../../etc/hosts + +# Known CVEs and Vulnerabilities +# +# Strings that test for known vulnerabilities + +() { 0; }; touch /tmp/blns.shellshock1.fail; +() { _; } >_[$($())] { touch /tmp/blns.shellshock2.fail; } + +# MSDOS/Windows Special Filenames +# +# Strings which are reserved characters in MSDOS/Windows + +CON +PRN +AUX +CLOCK$ +NUL +A: +ZZ: +COM1 +LPT1 +LPT2 +LPT3 +COM2 +COM3 +COM4 + +# Scunthorpe Problem +# +# Innocuous strings which may be blocked by profanity filters (https://en.wikipedia.org/wiki/Scunthorpe_problem) + +Scunthorpe General Hospital +Penistone Community Church +Lightwater Country Park +Jimmy Clitheroe +Horniman Museum +shitake mushrooms +RomansInSussex.co.uk +http://www.cum.qc.ca/ +Craig Cockburn, Software Specialist +Linda Callahan +Dr. Herman I. Libshitz +magna cum laude +Super Bowl XXX +medieval erection of parapets +evaluate +mocha +expression +Arsenal canal +classic +Tyson Gay + +# Human injection +# +# Strings which may cause human to reinterpret worldview + +If you're reading this, you've been in a coma for almost 20 years now. We're trying a new technique. We don't know where this message will end up in your dream, but we hope it works. Please wake up, we miss you. + +# Terminal escape codes +# +# Strings which punish the fools who use cat/type on this file + +Roses are red, violets are blue. Hope you enjoy terminal hue +But now...for my greatest trick... +The quick brown fox... [Beeeep] + +# iOS Vulnerability +# +# Strings which crashed iMessage in iOS versions 8.3 and earlier + +Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗 + + +\end{document} diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index d6678b94..87f424b0 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -191,7 +191,7 @@ describe "CompileManager", -> it "should run the texcount command", -> @directory = "#{@Settings.path.compilesDir}/#{@project_id}" @file_path = "$COMPILE_DIR/#{@file_name}" - @command =[ "texcount", '-inc', @file_path, "-out=" + @file_path + ".wc"] + @command =[ "texcount", "-inc", @file_path, "-out=" + @file_path + ".wc"] @CommandRunner.run .calledWith(@project_id, @command, @directory, @timeout) From d83efdbc982455d455c5cf3b2eff9456b35d0a43 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 17 Sep 2015 10:30:12 +0100 Subject: [PATCH 064/709] lock down smoke test and metrics version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc75cede..f2157177 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "request": "~2.21.0", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0", "sequelize": "^2.1.3", "wrench": "~1.5.4", - "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git", + "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", "sqlite3": "~2.2.0", "express": "^4.2.0", "body-parser": "^1.2.0", From 29be2dc70016689d65fe351a73ef13477dbea607 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Mon, 21 Sep 2015 14:04:08 +0100 Subject: [PATCH 065/709] When serving output files, intelligently determine the appropriate content-type. cherry pick 6fa3fda3ed28239cf3ac9720629f9707663aa197 from datajoy. --- app.coffee | 7 +-- app/coffee/ContentTypeMapper.coffee | 26 ++++++++++ .../unit/coffee/ContentTypeMapperTests.coffee | 51 +++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 app/coffee/ContentTypeMapper.coffee create mode 100644 test/unit/coffee/ContentTypeMapperTests.coffee diff --git a/app.coffee b/app.coffee index 8b4e2a9d..bd0a5864 100644 --- a/app.coffee +++ b/app.coffee @@ -3,6 +3,7 @@ Settings = require "settings-sharelatex" logger = require "logger-sharelatex" logger.initialize("clsi") smokeTest = require "smoke-test-sharelatex" +ContentTypeMapper = require "./app/js/ContentTypeMapper" Path = require "path" fs = require "fs" @@ -46,17 +47,13 @@ ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks" # and serving the files staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHeaders: (res, path, stat) -> if Path.basename(path) == "output.pdf" - res.set("Content-Type", "application/pdf") # Calculate an etag in the same way as nginx # https://github.com/tj/send/issues/65 etag = (path, stat) -> '"' + Math.ceil(+stat.mtime / 1000).toString(16) + '-' + Number(stat.size).toString(16) + '"' res.set("Etag", etag(path, stat)) - else - # Force plain treatment of other file types to prevent hosting of HTTP/JS files - # that could be used in same-origin/XSS attacks. - res.set("Content-Type", "text/plain") + res.set("Content-Type", ContentTypeMapper.map(path)) app.get "/project/:project_id/output/*", (req, res, next) -> if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX) diff --git a/app/coffee/ContentTypeMapper.coffee b/app/coffee/ContentTypeMapper.coffee new file mode 100644 index 00000000..4f0eb2d1 --- /dev/null +++ b/app/coffee/ContentTypeMapper.coffee @@ -0,0 +1,26 @@ +Path = require 'path' + +# here we coerce html, css and js to text/plain, +# otherwise choose correct mime type based on file extension, +# falling back to octet-stream +module.exports = ContentTypeMapper = + map: (path) -> + switch Path.extname(path) + when '.txt', '.html', '.js', '.css' + return 'text/plain' + when '.csv' + return 'text/csv' + when '.pdf' + return 'application/pdf' + when '.png' + return 'image/png' + when '.jpg', '.jpeg' + return 'image/jpeg' + when '.tiff' + return 'image/tiff' + when '.gif' + return 'image/gif' + when '.svg' + return 'image/svg+xml' + else + return 'application/octet-stream' diff --git a/test/unit/coffee/ContentTypeMapperTests.coffee b/test/unit/coffee/ContentTypeMapperTests.coffee new file mode 100644 index 00000000..d201b869 --- /dev/null +++ b/test/unit/coffee/ContentTypeMapperTests.coffee @@ -0,0 +1,51 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/ContentTypeMapper' + +describe 'ContentTypeMapper', -> + + beforeEach -> + @ContentTypeMapper = SandboxedModule.require modulePath + + describe 'map', -> + + it 'should map .txt to text/plain', -> + content_type = @ContentTypeMapper.map('example.txt') + content_type.should.equal 'text/plain' + + it 'should map .csv to text/csv', -> + content_type = @ContentTypeMapper.map('example.csv') + content_type.should.equal 'text/csv' + + it 'should map .pdf to application/pdf', -> + content_type = @ContentTypeMapper.map('example.pdf') + content_type.should.equal 'application/pdf' + + it 'should fall back to octet-stream', -> + content_type = @ContentTypeMapper.map('example.unknown') + content_type.should.equal 'application/octet-stream' + + describe 'coercing web files to plain text', -> + + it 'should map .js to plain text', -> + content_type = @ContentTypeMapper.map('example.js') + content_type.should.equal 'text/plain' + + it 'should map .html to plain text', -> + content_type = @ContentTypeMapper.map('example.html') + content_type.should.equal 'text/plain' + + it 'should map .css to plain text', -> + content_type = @ContentTypeMapper.map('example.css') + content_type.should.equal 'text/plain' + + describe 'image files', -> + + it 'should map .png to image/png', -> + content_type = @ContentTypeMapper.map('example.png') + content_type.should.equal 'image/png' + + it 'should map .jpeg to image/jpeg', -> + content_type = @ContentTypeMapper.map('example.jpeg') + content_type.should.equal 'image/jpeg' From 3ed29b34895faf49d5c0dfae010213955ea5d514 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 21 Oct 2015 10:02:30 +0100 Subject: [PATCH 066/709] increased cache time to 1.5 days --- app/coffee/ProjectPersistenceManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 2e23d46c..2767dce9 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -5,7 +5,7 @@ async = require "async" logger = require "logger-sharelatex" module.exports = ProjectPersistenceManager = - EXPIRY_TIMEOUT: oneDay = 24 * 60 * 60 * 1000 #ms + EXPIRY_TIMEOUT: (oneDay = 24 * 60 * 60 * 1000) * 1.5 #ms markProjectAsJustAccessed: (project_id, callback = (error) ->) -> db.Project.findOrCreate(where: {project_id: project_id}) From c178458223a6b7331d2566eba7a365f5a39769ef Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 12 Nov 2015 15:19:22 +0000 Subject: [PATCH 067/709] added try catch around word count where a file is not created --- app/coffee/CompileManager.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 296522f6..945da42e 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -120,7 +120,12 @@ module.exports = CompileManager = CommandRunner.run project_id, command, directory, timeout, (error) -> return callback(error) if error? - stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") + try + stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") + catch err + logger.err err:err, command:command, directory:directory, project_id:project_id, "error reading word count output" + return callback(err) + console.log "rooooof" callback null, CompileManager._parseWordcountFromOutput(stdout) _parseWordcountFromOutput: (output) -> From 2b5e7be964969e13a860b34d9ba8f8f91d668ef6 Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Thu, 3 Dec 2015 14:54:48 +0000 Subject: [PATCH 068/709] Remove undefined reference to dst --- app/coffee/OutputCacheManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 66abc4ea..5aca4a5c 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -40,7 +40,7 @@ module.exports = OutputCacheManager = callback(err) else if not stats.isFile() # other filetype - reject it - logger.error err: err, src: src, dst: dst, stat: stats, "nonfile output - refusing to copy to cache" + logger.error err: err, src: src, stat: stats, "nonfile output - refusing to copy to cache" callback(new Error("output file is not a file"), file) else # it's a plain file, ok to copy From 0ea28710f56d8b699a5316f21f7e8c3763c6c303 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 15 Dec 2015 19:33:37 +0000 Subject: [PATCH 069/709] fixed missing value in logger --- app/coffee/OutputCacheManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 66abc4ea..5aca4a5c 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -40,7 +40,7 @@ module.exports = OutputCacheManager = callback(err) else if not stats.isFile() # other filetype - reject it - logger.error err: err, src: src, dst: dst, stat: stats, "nonfile output - refusing to copy to cache" + logger.error err: err, src: src, stat: stats, "nonfile output - refusing to copy to cache" callback(new Error("output file is not a file"), file) else # it's a plain file, ok to copy From 4497352a3af92224f5a03f71a5038861a16e861b Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Tue, 12 Jan 2016 17:04:42 +0000 Subject: [PATCH 070/709] Allow optional image name to be passed --- app/coffee/CommandRunner.coffee | 2 +- app/coffee/CompileManager.coffee | 1 + app/coffee/LatexRunner.coffee | 4 ++-- app/coffee/RequestParser.coffee | 4 +++- test/unit/coffee/CompileManagerTests.coffee | 2 ++ test/unit/coffee/LatexRunnerTests.coffee | 7 +++++-- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 55dec333..41cbfaa8 100644 --- a/app/coffee/CommandRunner.coffee +++ b/app/coffee/CommandRunner.coffee @@ -2,7 +2,7 @@ spawn = require("child_process").spawn logger = require "logger-sharelatex" module.exports = CommandRunner = - run: (project_id, command, directory, timeout, callback = (error) ->) -> + run: (project_id, command, directory, image, timeout, 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" diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 945da42e..e7e88499 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -28,6 +28,7 @@ module.exports = CompileManager = mainFile: request.rootResourcePath compiler: request.compiler timeout: request.timeout + image: request.imageName }, (error) -> return callback(error) if error? logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "done compile" diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index cd6e3561..169a216f 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -6,7 +6,7 @@ CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner") module.exports = LatexRunner = runLatex: (project_id, options, callback = (error) ->) -> - {directory, mainFile, compiler, timeout} = options + {directory, mainFile, compiler, timeout, image} = options compiler ||= "pdflatex" timeout ||= 60000 # milliseconds @@ -27,7 +27,7 @@ module.exports = LatexRunner = else return callback new Error("unknown compiler: #{compiler}") - CommandRunner.run project_id, command, directory, timeout, callback + CommandRunner.run project_id, command, directory, image, timeout, callback _latexmkBaseCommand: [ "latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 53268103..1b4e06d9 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -21,6 +21,9 @@ module.exports = RequestParser = compile.options.timeout default: RequestParser.MAX_TIMEOUT type: "number" + response.imageName = @_parseAttribute "imageName", + compile.options.imageName, + type: "string" if response.timeout > RequestParser.MAX_TIMEOUT response.timeout = RequestParser.MAX_TIMEOUT @@ -71,7 +74,6 @@ module.exports = RequestParser = throw "#{name} attribute should be a #{options.type}" else return options.default if options.default? - throw "Default not implemented" return attribute _sanitizePath: (path) -> diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 87f424b0..dff30524 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -44,6 +44,7 @@ describe "CompileManager", -> project_id: @project_id = "project-id-123" compiler: @compiler = "pdflatex" timeout: @timeout = 42000 + imageName: @image = "example.com/image" @Settings.compileDir = "compiles" @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}" @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArg(3) @@ -64,6 +65,7 @@ describe "CompileManager", -> mainFile: @rootResourcePath compiler: @compiler timeout: @timeout + image: @image }) .should.equal true diff --git a/test/unit/coffee/LatexRunnerTests.coffee b/test/unit/coffee/LatexRunnerTests.coffee index d3782a6f..ace3d18a 100644 --- a/test/unit/coffee/LatexRunnerTests.coffee +++ b/test/unit/coffee/LatexRunnerTests.coffee @@ -19,12 +19,13 @@ describe "LatexRunner", -> @directory = "/local/compile/directory" @mainFile = "main-file.tex" @compiler = "pdflatex" + @image = "example.com/image" @callback = sinon.stub() @project_id = "project-id-123" describe "runLatex", -> beforeEach -> - @CommandRunner.run = sinon.stub().callsArg(4) + @CommandRunner.run = sinon.stub().callsArg(5) describe "normally", -> beforeEach -> @@ -33,11 +34,12 @@ describe "LatexRunner", -> mainFile: @mainFile compiler: @compiler timeout: @timeout = 42000 + image: @image @callback it "should run the latex command", -> @CommandRunner.run - .calledWith(@project_id, sinon.match.any, @directory, @timeout) + .calledWith(@project_id, sinon.match.any, @directory, @image, @timeout) .should.equal true describe "with an .Rtex main file", -> @@ -46,6 +48,7 @@ describe "LatexRunner", -> directory: @directory mainFile: "main-file.Rtex" compiler: @compiler + image: @image timeout: @timeout = 42000 @callback From 86cf05c7328b172899e4e56a5ad7ce4e531cf08b Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Tue, 19 Jan 2016 14:12:41 +0000 Subject: [PATCH 071/709] Support configurable images in wordcount end point --- app/coffee/CompileController.coffee | 4 +++- app/coffee/CompileManager.coffee | 6 +++--- test/unit/coffee/CompileControllerTests.coffee | 5 +++-- test/unit/coffee/CompileManagerTests.coffee | 7 ++++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index f7d46b13..dd730409 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -70,8 +70,10 @@ module.exports = CompileController = wordcount: (req, res, next = (error) ->) -> file = req.query.file || "main.tex" project_id = req.params.project_id + image = req.query.image + logger.log {image, file, project_id}, "word count request" - CompileManager.wordcount project_id, file, (error, result) -> + CompileManager.wordcount project_id, file, image, (error, result) -> return next(error) if error? res.send JSON.stringify { texcount: result diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index e7e88499..20e9bc96 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -112,14 +112,14 @@ module.exports = CompileManager = } return results - wordcount: (project_id, file_name, callback = (error, pdfPositions) ->) -> - logger.log project_id:project_id, file_name:file_name, "running wordcount" + wordcount: (project_id, file_name, image, callback = (error, pdfPositions) ->) -> + logger.log project_id:project_id, file_name:file_name, image:image, "running wordcount" file_path = "$COMPILE_DIR/" + file_name command = [ "texcount", '-inc', file_path, "-out=" + file_path + ".wc"] directory = Path.join(Settings.path.compilesDir, project_id) timeout = 10 * 1000 - CommandRunner.run project_id, command, directory, timeout, (error) -> + CommandRunner.run project_id, command, directory, image, timeout, (error) -> return callback(error) if error? try stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8") diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index e0307d20..6a18ce79 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -197,14 +197,15 @@ describe "CompileController", -> project_id: @project_id @req.query = file: @file + image: @image = "example.com/image" @res.send = sinon.stub() - @CompileManager.wordcount = sinon.stub().callsArgWith(2, null, @texcount = ["mock-texcount"]) + @CompileManager.wordcount = sinon.stub().callsArgWith(3, null, @texcount = ["mock-texcount"]) @CompileController.wordcount @req, @res, @next it "should return the word count of a file", -> @CompileManager.wordcount - .calledWith(@project_id, @file) + .calledWith(@project_id, @file, @image) .should.equal true it "should return the texcount info", -> diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index dff30524..54576d8e 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -179,7 +179,7 @@ describe "CompileManager", -> describe "wordcount", -> beforeEach -> - @CommandRunner.run = sinon.stub().callsArg(4) + @CommandRunner.run = sinon.stub().callsArg(5) @fs.readFileSync = sinon.stub().returns @stdout = "Encoding: ascii\nWords in text: 2" @callback = sinon.stub() @@ -187,8 +187,9 @@ describe "CompileManager", -> @timeout = 10 * 1000 @file_name = "main.tex" @Settings.path.compilesDir = "/local/compile/directory" + @image = "example.com/image" - @CompileManager.wordcount @project_id, @file_name, @callback + @CompileManager.wordcount @project_id, @file_name, @image, @callback it "should run the texcount command", -> @directory = "#{@Settings.path.compilesDir}/#{@project_id}" @@ -196,7 +197,7 @@ describe "CompileManager", -> @command =[ "texcount", "-inc", @file_path, "-out=" + @file_path + ".wc"] @CommandRunner.run - .calledWith(@project_id, @command, @directory, @timeout) + .calledWith(@project_id, @command, @directory, @image, @timeout) .should.equal true it "should call the callback with the parsed output", -> From 03b75b12cfad9b8d0ddfc16038e03a6cc33685ff Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Mon, 1 Feb 2016 13:19:16 +0000 Subject: [PATCH 072/709] Download up to 5 files in parallel --- app/coffee/ResourceWriter.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 25b2625f..a8b01066 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -14,7 +14,7 @@ module.exports = ResourceWriter = jobs = for resource in resources do (resource) => (callback) => @_writeResourceToDisk(project_id, resource, basePath, callback) - async.series jobs, callback + async.parallelLimit jobs, 5, callback _removeExtraneousFiles: (resources, basePath, _callback = (error) ->) -> timer = new Metrics.Timer("unlink-output-files") From d96605d5e8e46977451df1d523d0d8b4a82442aa Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Tue, 2 Feb 2016 14:26:14 +0000 Subject: [PATCH 073/709] Inject [draft] option to documentclass if draft option is passed --- app/coffee/CompileController.coffee | 1 + app/coffee/CompileManager.coffee | 41 ++++++++----- app/coffee/DraftModeManager.coffee | 21 +++++++ app/coffee/RequestParser.coffee | 4 ++ test/unit/coffee/CompileManagerTests.coffee | 60 +++++++++++------- test/unit/coffee/DraftModeManagerTests.coffee | 61 +++++++++++++++++++ 6 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 app/coffee/DraftModeManager.coffee create mode 100644 test/unit/coffee/DraftModeManagerTests.coffee diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index dd730409..7ee9eefb 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -11,6 +11,7 @@ module.exports = CompileController = RequestParser.parse req.body, (error, request) -> return next(error) if error? request.project_id = req.params.project_id + logger.log {request}, "got request" ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? CompileManager.doCompile request, (error, outputFiles = []) -> diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 20e9bc96..82ac793d 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -8,6 +8,7 @@ logger = require "logger-sharelatex" Metrics = require "./Metrics" child_process = require "child_process" CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner") +DraftModeManager = require "./DraftModeManager" fs = require("fs") module.exports = CompileManager = @@ -20,24 +21,32 @@ module.exports = CompileManager = return callback(error) if error? logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "written files to disk" timer.done() - - timer = new Metrics.Timer("run-compile") - Metrics.inc("compiles") - LatexRunner.runLatex request.project_id, { - directory: compileDir - mainFile: request.rootResourcePath - compiler: request.compiler - timeout: request.timeout - image: request.imageName - }, (error) -> + + injectDraftModeIfRequired = (callback) -> + if request.draft + DraftModeManager.injectDraftMode Path.join(compileDir, request.rootResourcePath), callback + else + callback() + + injectDraftModeIfRequired (error) -> return callback(error) if error? - logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "done compile" - timer.done() - - OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> + timer = new Metrics.Timer("run-compile") + Metrics.inc("compiles") + LatexRunner.runLatex request.project_id, { + directory: compileDir + mainFile: request.rootResourcePath + compiler: request.compiler + timeout: request.timeout + image: request.imageName + }, (error) -> return callback(error) if error? - OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> - callback null, newOutputFiles + logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "done compile" + timer.done() + + OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> + return callback(error) if error? + OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> + callback null, newOutputFiles clearProject: (project_id, _callback = (error) ->) -> callback = (error) -> diff --git a/app/coffee/DraftModeManager.coffee b/app/coffee/DraftModeManager.coffee new file mode 100644 index 00000000..a0d859d7 --- /dev/null +++ b/app/coffee/DraftModeManager.coffee @@ -0,0 +1,21 @@ +fs = require "fs" +logger = require "logger-sharelatex" + +module.exports = DraftModeManager = + injectDraftMode: (filename, callback = (error) ->) -> + fs.readFile filename, "utf8", (error, content) -> + return callback(error) if error? + modified_content = DraftModeManager._injectDraftOption content + logger.log { + content: content.slice(0,1024), # \documentclass is normally v near the top + modified_content: modified_content.slice(0,1024), + filename + }, "injected draft class" + fs.writeFile filename, modified_content, callback + + _injectDraftOption: (content) -> + content + # With existing options (must be first, otherwise both are applied) + .replace(/\\documentclass\[/, "\\documentclass[draft,") + # Without existing options + .replace(/\\documentclass\{/, "\\documentclass[draft]{") \ No newline at end of file diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 1b4e06d9..bd081fd0 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -24,6 +24,10 @@ module.exports = RequestParser = response.imageName = @_parseAttribute "imageName", compile.options.imageName, type: "string" + response.draft = @_parseAttribute "draft", + compile.options.draft, + default: false, + type: "boolean" if response.timeout > RequestParser.MAX_TIMEOUT response.timeout = RequestParser.MAX_TIMEOUT diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 54576d8e..018e01d2 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -17,6 +17,7 @@ describe "CompileManager", -> "logger-sharelatex": @logger = { log: sinon.stub() } "child_process": @child_process = {} "./CommandRunner": @CommandRunner = {} + "./DraftModeManager": @DraftModeManager = {} "fs": @fs = {} @callback = sinon.stub() @@ -51,31 +52,48 @@ describe "CompileManager", -> @LatexRunner.runLatex = sinon.stub().callsArg(2) @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) - @CompileManager.doCompile @request, @callback + @DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) + + describe "normally", -> + beforeEach -> + @CompileManager.doCompile @request, @callback - it "should write the resources to disk", -> - @ResourceWriter.syncResourcesToDisk - .calledWith(@project_id, @resources, @compileDir) - .should.equal true + it "should write the resources to disk", -> + @ResourceWriter.syncResourcesToDisk + .calledWith(@project_id, @resources, @compileDir) + .should.equal true - it "should run LaTeX", -> - @LatexRunner.runLatex - .calledWith(@project_id, { - directory: @compileDir - mainFile: @rootResourcePath - compiler: @compiler - timeout: @timeout - image: @image - }) - .should.equal true + it "should run LaTeX", -> + @LatexRunner.runLatex + .calledWith(@project_id, { + directory: @compileDir + mainFile: @rootResourcePath + compiler: @compiler + timeout: @timeout + image: @image + }) + .should.equal true - it "should find the output files", -> - @OutputFileFinder.findOutputFiles - .calledWith(@resources, @compileDir) - .should.equal true + it "should find the output files", -> + @OutputFileFinder.findOutputFiles + .calledWith(@resources, @compileDir) + .should.equal true - it "should return the output files", -> - @callback.calledWith(null, @build_files).should.equal true + it "should return the output files", -> + @callback.calledWith(null, @build_files).should.equal true + + it "should not inject draft mode by default", -> + @DraftModeManager.injectDraftMode.called.should.equal false + + describe "with draft mode", -> + beforeEach -> + @request.draft = true + @CompileManager.doCompile @request, @callback + + it "should inject the draft mode header", -> + @DraftModeManager.injectDraftMode + .calledWith(@compileDir + "/" + @rootResourcePath) + .should.equal true describe "clearProject", -> describe "succesfully", -> diff --git a/test/unit/coffee/DraftModeManagerTests.coffee b/test/unit/coffee/DraftModeManagerTests.coffee new file mode 100644 index 00000000..549be293 --- /dev/null +++ b/test/unit/coffee/DraftModeManagerTests.coffee @@ -0,0 +1,61 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/DraftModeManager' + +describe 'DraftModeManager', -> + beforeEach -> + @DraftModeManager = SandboxedModule.require modulePath, requires: + "fs": @fs = {} + "logger-sharelatex": @logger = {log: () ->} + + describe "_injectDraftOption", -> + it "should add draft option into documentclass with existing options", -> + @DraftModeManager + ._injectDraftOption(''' + \\documentclass[a4paper,foo=bar]{article} + ''') + .should.equal(''' + \\documentclass[draft,a4paper,foo=bar]{article} + ''') + + it "should add draft option into documentclass with no options", -> + @DraftModeManager + ._injectDraftOption(''' + \\documentclass{article} + ''') + .should.equal(''' + \\documentclass[draft]{article} + ''') + + describe "injectDraftMode", -> + beforeEach -> + @filename = "/mock/filename.tex" + @callback = sinon.stub() + content = ''' + \\documentclass{article} + \\begin{document} + Hello world + \\end{document} + ''' + @fs.readFile = sinon.stub().callsArgWith(2, null, content) + @fs.writeFile = sinon.stub().callsArg(2) + @DraftModeManager.injectDraftMode @filename, @callback + + it "should read the file", -> + @fs.readFile + .calledWith(@filename, "utf8") + .should.equal true + + it "should write the modified file", -> + @fs.writeFile + .calledWith(@filename, """ + \\documentclass[draft]{article} + \\begin{document} + Hello world + \\end{document} + """) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true From 2df886e33011fdd028c3135a6700e6d3d587d8b9 Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Tue, 2 Feb 2016 14:28:51 +0000 Subject: [PATCH 074/709] Remove left over debug log line --- app/coffee/CompileController.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 7ee9eefb..dd730409 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -11,7 +11,6 @@ module.exports = CompileController = RequestParser.parse req.body, (error, request) -> return next(error) if error? request.project_id = req.params.project_id - logger.log {request}, "got request" ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? CompileManager.doCompile request, (error, outputFiles = []) -> From a3383f11a127cd8862459fea5866f13952ac5da1 Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Tue, 2 Feb 2016 15:28:59 +0000 Subject: [PATCH 075/709] Make draft mode regex global --- app/coffee/DraftModeManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/DraftModeManager.coffee b/app/coffee/DraftModeManager.coffee index a0d859d7..253cbffd 100644 --- a/app/coffee/DraftModeManager.coffee +++ b/app/coffee/DraftModeManager.coffee @@ -16,6 +16,6 @@ module.exports = DraftModeManager = _injectDraftOption: (content) -> content # With existing options (must be first, otherwise both are applied) - .replace(/\\documentclass\[/, "\\documentclass[draft,") + .replace(/\\documentclass\[/g, "\\documentclass[draft,") # Without existing options - .replace(/\\documentclass\{/, "\\documentclass[draft]{") \ No newline at end of file + .replace(/\\documentclass\{/g, "\\documentclass[draft]{") \ No newline at end of file From 89acd36dde573905cc087018ff3fbaa539e5b76e Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Thu, 10 Mar 2016 09:32:32 +0000 Subject: [PATCH 076/709] Send .svg files as text/plain to prevent executable JS if they are loaded as SVG in the browser --- app/coffee/ContentTypeMapper.coffee | 4 +--- test/unit/coffee/ContentTypeMapperTests.coffee | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/coffee/ContentTypeMapper.coffee b/app/coffee/ContentTypeMapper.coffee index 4f0eb2d1..68b2d14f 100644 --- a/app/coffee/ContentTypeMapper.coffee +++ b/app/coffee/ContentTypeMapper.coffee @@ -6,7 +6,7 @@ Path = require 'path' module.exports = ContentTypeMapper = map: (path) -> switch Path.extname(path) - when '.txt', '.html', '.js', '.css' + when '.txt', '.html', '.js', '.css', '.svg' return 'text/plain' when '.csv' return 'text/csv' @@ -20,7 +20,5 @@ module.exports = ContentTypeMapper = return 'image/tiff' when '.gif' return 'image/gif' - when '.svg' - return 'image/svg+xml' else return 'application/octet-stream' diff --git a/test/unit/coffee/ContentTypeMapperTests.coffee b/test/unit/coffee/ContentTypeMapperTests.coffee index d201b869..2439120b 100644 --- a/test/unit/coffee/ContentTypeMapperTests.coffee +++ b/test/unit/coffee/ContentTypeMapperTests.coffee @@ -49,3 +49,7 @@ describe 'ContentTypeMapper', -> it 'should map .jpeg to image/jpeg', -> content_type = @ContentTypeMapper.map('example.jpeg') content_type.should.equal 'image/jpeg' + + it 'should map .svg to text/plain to protect against XSS (SVG can execute JS)', -> + content_type = @ContentTypeMapper.map('example.svg') + content_type.should.equal 'text/plain' From dcfe1118d4449e9935fe77c217fd6e376302bad0 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 10 Mar 2016 10:30:37 +0000 Subject: [PATCH 077/709] increased EXPIRY_TIMEOUT from 1.5 days to 2.5 days --- app/coffee/ProjectPersistenceManager.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 2767dce9..9f11cf31 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -3,9 +3,11 @@ CompileManager = require "./CompileManager" db = require "./db" async = require "async" logger = require "logger-sharelatex" +oneDay = 24 * 60 * 60 * 1000 module.exports = ProjectPersistenceManager = - EXPIRY_TIMEOUT: (oneDay = 24 * 60 * 60 * 1000) * 1.5 #ms + + EXPIRY_TIMEOUT: oneDay * 2.5 markProjectAsJustAccessed: (project_id, callback = (error) ->) -> db.Project.findOrCreate(where: {project_id: project_id}) From e64b08fcbe0ed539d353e4b585a51a6b5c27e4d7 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 15 Mar 2016 14:04:37 +0000 Subject: [PATCH 078/709] add metrics for latexmk runs and errors --- app/coffee/CompileManager.coffee | 14 +++++++++++--- app/coffee/LatexRunner.coffee | 15 +++++++++++++-- package.json | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 82ac793d..f3330182 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -10,6 +10,7 @@ child_process = require "child_process" CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner") DraftModeManager = require "./DraftModeManager" fs = require("fs") +os = require("os") module.exports = CompileManager = doCompile: (request, callback = (error, outputFiles) ->) -> @@ -38,10 +39,17 @@ module.exports = CompileManager = compiler: request.compiler timeout: request.timeout image: request.imageName - }, (error) -> + }, (error, output, stats) -> return callback(error) if error? - logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "done compile" - timer.done() + Metrics.inc("compiles") + for metric_key, metric_value of stats or {} + Metrics.count(metric_key, metric_value) + loadavg = os.loadavg?() + Metrics.gauge("load-avg", loadavg[0]) if loadavg? + ts = timer.done() + logger.log {project_id: request.project_id, time_taken: ts, stats:stats, loadavg:loadavg}, "done compile" + if stats?["latex-runs"] > 0 + Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"]) OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> return callback(error) if error? diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 169a216f..1cd851ba 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -27,9 +27,20 @@ module.exports = LatexRunner = else return callback new Error("unknown compiler: #{compiler}") - CommandRunner.run project_id, command, directory, image, timeout, callback + CommandRunner.run project_id, command, directory, image, timeout, (error, output) -> + 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 + # counters from latexmk output + stats = {} + stats["latexmk-errors"] = failed + stats["latex-runs"] = runs + stats["latex-runs-with-errors"] = if failed then runs else 0 + stats["latex-runs-#{runs}"] = 1 + stats["latex-runs-with-errors-#{runs}"] = if failed then 1 else 0 + callback error, output, stats - _latexmkBaseCommand: [ "latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] + _latexmkBaseCommand: ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] _pdflatexCommand: (mainFile) -> LatexRunner._latexmkBaseCommand.concat [ diff --git a/package.json b/package.json index f2157177..33560b59 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "request": "~2.21.0", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", "sequelize": "^2.1.3", "wrench": "~1.5.4", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", From 595bfe09ac99e9daed3e3719b0c49977e42cea30 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 16 Mar 2016 15:49:13 +0000 Subject: [PATCH 079/709] add metric for qpdf --- app/coffee/OutputFileOptimiser.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/coffee/OutputFileOptimiser.coffee b/app/coffee/OutputFileOptimiser.coffee index f337a7a9..eb5266e1 100644 --- a/app/coffee/OutputFileOptimiser.coffee +++ b/app/coffee/OutputFileOptimiser.coffee @@ -2,6 +2,7 @@ fs = require "fs" Path = require "path" spawn = require("child_process").spawn logger = require "logger-sharelatex" +Metrics = require "./Metrics" _ = require "underscore" module.exports = OutputFileOptimiser = @@ -19,6 +20,7 @@ module.exports = OutputFileOptimiser = args = ["--linearize", src, tmpOutput] logger.log args: args, "running qpdf command" + timer = new Metrics.Timer("qpdf") proc = spawn("qpdf", args) stdout = "" proc.stdout.on "data", (chunk) -> @@ -28,6 +30,7 @@ module.exports = OutputFileOptimiser = logger.warn {err, args}, "qpdf failed" callback(null) # ignore the error proc.on "close", (code) -> + timer.done() if code != 0 logger.warn {code, args}, "qpdf returned error" return callback(null) # ignore the error From 9f104a4f57ec813fe260902599b8e5a0c10d92f9 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 17 Mar 2016 14:37:34 +0000 Subject: [PATCH 080/709] bugfix - avoid double counting compiles --- app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index f3330182..a193ac9f 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -41,7 +41,7 @@ module.exports = CompileManager = image: request.imageName }, (error, output, stats) -> return callback(error) if error? - Metrics.inc("compiles") + Metrics.inc("compiles-succeeded") for metric_key, metric_value of stats or {} Metrics.count(metric_key, metric_value) loadavg = os.loadavg?() From 6af22cf18407b202fa4c731a9a2d05be8c2ad92a Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Tue, 29 Mar 2016 16:45:06 +0100 Subject: [PATCH 081/709] Add in flags to run strace and capture logs --- app/coffee/LatexRunner.coffee | 3 + app/coffee/OutputCacheManager.coffee | 101 +++++++++++++++++++-------- 2 files changed, 76 insertions(+), 28 deletions(-) diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 1cd851ba..938d4a51 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -26,6 +26,9 @@ module.exports = LatexRunner = command = LatexRunner._lualatexCommand mainFile else return callback new Error("unknown compiler: #{compiler}") + + if Settings.clsi?.strace + command = ["strace", "-o", "strace-#{Date.now()}", "-ff"].concat(command) CommandRunner.run project_id, command, directory, image, timeout, (error, output) -> return callback(error) if error? diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 5aca4a5c..1517ccc3 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -4,11 +4,13 @@ fse = require "fs-extra" Path = require "path" logger = require "logger-sharelatex" _ = require "underscore" +Settings = require "settings-sharelatex" OutputFileOptimiser = require "./OutputFileOptimiser" module.exports = OutputCacheManager = CACHE_SUBDIR: '.cache/clsi' + ARCHIVE_SUBDIR: '.archive/clsi' BUILD_REGEX: /^[0-9a-f]+$/ # build id is Date.now() converted to hex CACHE_LIMIT: 2 # maximum number of cache directories CACHE_AGE: 60*60*1000 # up to one hour old @@ -28,33 +30,15 @@ module.exports = OutputCacheManager = # Put the files into a new cache subdirectory buildId = Date.now().toString(16) cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId) + # let file expiry run in the background OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId} - checkFile = (src, callback) -> - # check if we have a valid file to copy into the cache - fs.stat src, (err, stats) -> - if err? - # some problem reading the file - logger.error err: err, file: src, "stat error for file in cache" - callback(err) - else if not stats.isFile() - # other filetype - reject it - logger.error err: err, src: src, stat: stats, "nonfile output - refusing to copy to cache" - callback(new Error("output file is not a file"), file) - else - # it's a plain file, ok to copy - callback(null) - - copyFile = (src, dst, callback) -> - # copy output file into the cache - fse.copy src, dst, (err) -> + # Archive logs in background + if Settings.clsi?.archive_logs + OutputCacheManager.archiveLogs outputFiles, compileDir, (err) -> if err? - logger.error err: err, src: src, dst: dst, "copy error for file in cache" - callback(err) - else - # call the optimiser for the file too - OutputFileOptimiser.optimiseFile src, dst, callback + logger.warn err:err, "erroring archiving log files" # make the new cache directory fse.ensureDir cacheDir, (err) -> @@ -63,21 +47,47 @@ module.exports = OutputCacheManager = callback(err, outputFiles) else # copy all the output files into the new cache directory + results = [] async.mapSeries outputFiles, (file, cb) -> newFile = _.clone(file) [src, dst] = [Path.join(compileDir, file.path), Path.join(cacheDir, file.path)] - checkFile src, (err) -> - copyFile src, dst, (err) -> - if not err? + OutputCacheManager._checkFileIsSafe src, (err, isSafe) -> + return cb(err) if err? + if !isSafe + return cb() + OutputCacheManager._checkIfShouldCopy src, (err, shouldCopy) -> + return cb(err) if err? + if !shouldCopy + return cb() + OutputCacheManager._copyFile src, dst, (err) -> + return cb(err) if err? newFile.build = buildId # attach a build id if we cached the file - cb(err, newFile) - , (err, results) -> + results.push newFile + cb() + , (err) -> if err? # pass back the original files if we encountered *any* error callback(err, outputFiles) else # pass back the list of new files in the cache callback(err, results) + + archiveLogs: (outputFiles, compileDir, callback = (error) ->) -> + buildId = Date.now().toString(16) + archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId) + logger.log {dir: archiveDir}, "archiving log files for project" + fse.ensureDir archiveDir, (err) -> + return callback(err) if err? + async.mapSeries outputFiles, (file, cb) -> + [src, dst] = [Path.join(compileDir, file.path), Path.join(archiveDir, file.path)] + OutputCacheManager._checkFileIsSafe src, (err, isSafe) -> + return cb(err) if err? + return cb() if !isSafe + OutputCacheManager._checkIfShouldArchive src, (err, shouldArchive) -> + return cb(err) if err? + return cb() if !shouldArchive + OutputCacheManager._copyFile src, dst, cb + , callback expireOutputFiles: (cacheRoot, options, callback = (error) ->) -> # look in compileDir for build dirs and delete if > N or age of mod time > T @@ -111,3 +121,38 @@ module.exports = OutputCacheManager = async.eachSeries toRemove, (dir, cb) -> removeDir dir, cb , callback + + _checkFileIsSafe: (src, callback = (error, isSafe) ->) -> + # check if we have a valid file to copy into the cache + fs.stat src, (err, stats) -> + if err? + # some problem reading the file + logger.error err: err, file: src, "stat error for file in cache" + callback(err, false) + else if not stats.isFile() + # other filetype - reject it + logger.warn src: src, stat: stats, "nonfile output - refusing to copy to cache" + callback(null, false) + else + # it's a plain file, ok to copy + callback(null, true) + + _copyFile: (src, dst, callback) -> + # copy output file into the cache + fse.copy src, dst, (err) -> + if err? + logger.error err: err, src: src, dst: dst, "copy error for file in cache" + callback(err) + else + # call the optimiser for the file too + OutputFileOptimiser.optimiseFile src, dst, callback + + _checkIfShouldCopy: (src, callback = (err, shouldCopy) ->) -> + return callback(null, !Path.basename(src).match(/^strace/)) + + _checkIfShouldArchive: (src, callback = (err, shouldCopy) ->) -> + if Path.basename(src).match(/^strace/) + return callback(null, true) + if Path.basename(src).match(/^output\.(?!pdf)/) + return callback(null, true) + return callback(null, false) \ No newline at end of file From d949d4ac324ea77028c9af6556e05a3fb1afaa5e Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Wed, 30 Mar 2016 10:59:01 +0100 Subject: [PATCH 082/709] Don't timestamp strace logs otherwise it runs as anew container each time since the command changes --- app/coffee/LatexRunner.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 938d4a51..4280d95a 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -28,7 +28,7 @@ module.exports = LatexRunner = return callback new Error("unknown compiler: #{compiler}") if Settings.clsi?.strace - command = ["strace", "-o", "strace-#{Date.now()}", "-ff"].concat(command) + command = ["strace", "-o", "strace", "-ff"].concat(command) CommandRunner.run project_id, command, directory, image, timeout, (error, output) -> return callback(error) if error? From 6117cac1fdd6ed2688248629bceef45a85b9abd1 Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Wed, 30 Mar 2016 11:41:11 +0100 Subject: [PATCH 083/709] Ignore both .cache and .archive and other hidden files in finding output files --- app/coffee/OutputFileFinder.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index dcec5f5d..b4a91625 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -29,7 +29,7 @@ module.exports = OutputFileFinder = _callback(error, fileList) _callback = () -> - args = [directory, "-name", ".cache", "-prune", "-o", "-type", "f", "-print"] + args = [directory, "-name", ".*", "-prune", "-o", "-type", "f", "-print"] logger.log args: args, "running find command" proc = spawn("find", args) From fbb00ebf2feba9b09d823e813e348ec08dc86631 Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Wed, 30 Mar 2016 14:10:07 +0100 Subject: [PATCH 084/709] Only archive main log and blg --- app/coffee/OutputCacheManager.coffee | 4 ++-- config/settings.defaults.coffee | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 1517ccc3..5f7558a5 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -35,7 +35,7 @@ module.exports = OutputCacheManager = OutputCacheManager.expireOutputFiles cacheRoot, {keep: buildId} # Archive logs in background - if Settings.clsi?.archive_logs + if Settings.clsi?.archive_logs or Settings.clsi?.strace OutputCacheManager.archiveLogs outputFiles, compileDir, (err) -> if err? logger.warn err:err, "erroring archiving log files" @@ -153,6 +153,6 @@ module.exports = OutputCacheManager = _checkIfShouldArchive: (src, callback = (err, shouldCopy) ->) -> if Path.basename(src).match(/^strace/) return callback(null, true) - if Path.basename(src).match(/^output\.(?!pdf)/) + if Settings.clsi?.archive_logs and Path.basename(src) in ["output.log", "output.blg"] return callback(null, true) return callback(null, false) \ No newline at end of file diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 4169f275..8e589a55 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -17,6 +17,8 @@ module.exports = synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) # clsi: + # strace: true + # archive_logs: true # commandRunner: "docker-runner-sharelatex" # docker: # image: "quay.io/sharelatex/texlive-full" From 8fcbec5c0f4ae5ec0f0be3236a97f57848041ac3 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 30 Mar 2016 14:35:47 +0100 Subject: [PATCH 085/709] add support for sentry --- app.coffee | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index bd0a5864..7412c7f7 100644 --- a/app.coffee +++ b/app.coffee @@ -2,6 +2,9 @@ CompileController = require "./app/js/CompileController" Settings = require "settings-sharelatex" logger = require "logger-sharelatex" logger.initialize("clsi") +if Settings.sentry?.dsn? + logger.initializeErrorReporting(Settings.sentry.dsn) + smokeTest = require "smoke-test-sharelatex" ContentTypeMapper = require "./app/js/ContentTypeMapper" @@ -63,6 +66,11 @@ app.get "/project/:project_id/output/*", (req, res, next) -> req.url = "/#{req.params.project_id}/#{req.params[0]}" staticServer(req, res, next) +app.get "/oops", (req, res, next) -> + logger.error {err: "hello"}, "test error" + res.send "error\n" + + app.get "/status", (req, res, next) -> res.send "CLSI is alive\n" diff --git a/package.json b/package.json index 33560b59..b5b1c9b6 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "~2.21.0", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.4.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", "sequelize": "^2.1.3", From fe46a96fd2473fbbf62433a716fa9efb5d9a3aae Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 31 Mar 2016 11:14:39 +0100 Subject: [PATCH 086/709] don't log missing files as warnings, but do report file access errors --- app/coffee/StaticServerForbidSymlinks.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/StaticServerForbidSymlinks.coffee b/app/coffee/StaticServerForbidSymlinks.coffee index 83ca4ca7..1b3cd458 100644 --- a/app/coffee/StaticServerForbidSymlinks.coffee +++ b/app/coffee/StaticServerForbidSymlinks.coffee @@ -29,10 +29,10 @@ module.exports = ForbidSymlinks = (staticFn, root, options) -> # check that the requested path is not a symlink fs.realpath requestedFsPath, (err, realFsPath)-> if err? - logger.warn err:err, requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "error checking file access" if err.code == 'ENOENT' return res.sendStatus(404) else + logger.error err:err, requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "error checking file access" return res.sendStatus(500) else if requestedFsPath != realFsPath logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" From 3dcd4af62efb439f2041aa61d878e4a88d0f5717 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 31 Mar 2016 11:25:25 +0100 Subject: [PATCH 087/709] always create project directory when syncing resources to disk avoids errors when project is empty --- app/coffee/ResourceWriter.coffee | 23 ++++++++++++++++----- test/unit/coffee/ResourceWriterTests.coffee | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index a8b01066..22c8b9f7 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -9,12 +9,25 @@ logger = require "logger-sharelatex" module.exports = ResourceWriter = syncResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> - @_removeExtraneousFiles resources, basePath, (error) => + @_createDirectory basePath, (error) => return callback(error) if error? - jobs = for resource in resources - do (resource) => - (callback) => @_writeResourceToDisk(project_id, resource, basePath, callback) - async.parallelLimit jobs, 5, callback + @_removeExtraneousFiles resources, basePath, (error) => + return callback(error) if error? + jobs = for resource in resources + do (resource) => + (callback) => @_writeResourceToDisk(project_id, resource, basePath, callback) + async.parallelLimit jobs, 5, callback + + _createDirectory: (basePath, callback = (error) ->) -> + fs.mkdir basePath, (err) -> + if err? + if err.code is 'EEXIST' + return callback() + else + logger.log {err: err, dir:basePath}, "error creating directory" + return callback(err) + else + return callback() _removeExtraneousFiles: (resources, basePath, _callback = (error) ->) -> timer = new Metrics.Timer("unlink-output-files") diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 3e87b5ca..5480bd6b 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -7,7 +7,7 @@ path = require "path" describe "ResourceWriter", -> beforeEach -> @ResourceWriter = SandboxedModule.require modulePath, requires: - "fs": @fs = {} + "fs": @fs = { mkdir: sinon.stub().callsArg(1) } "wrench": @wrench = {} "./UrlCache" : @UrlCache = {} "mkdirp" : @mkdirp = sinon.stub().callsArg(1) From bd036534e5e95d60f8004ce74051afc16aa894af Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 31 Mar 2016 11:43:36 +0100 Subject: [PATCH 088/709] check directory exists before attempting to clear it --- app/coffee/CompileManager.coffee | 34 ++++++++++++++++----- test/unit/coffee/CompileManagerTests.coffee | 2 ++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index a193ac9f..faae9da9 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -62,18 +62,36 @@ module.exports = CompileManager = _callback = () -> compileDir = Path.join(Settings.path.compilesDir, project_id) - proc = child_process.spawn "rm", ["-r", compileDir] - proc.on "error", callback + CompileManager._checkDirectory compileDir, (err, exists) -> + return callback(err) if err? + return callback() if not exists # skip removal if no directory present - stderr = "" - proc.stderr.on "data", (chunk) -> stderr += chunk.toString() + proc = child_process.spawn "rm", ["-r", compileDir] - proc.on "close", (code) -> - if code == 0 - return callback(null) + proc.on "error", callback + + stderr = "" + proc.stderr.on "data", (chunk) -> stderr += chunk.toString() + + proc.on "close", (code) -> + if code == 0 + return callback(null) + else + return callback(new Error("rm -r #{compileDir} failed: #{stderr}")) + + _checkDirectory: (compileDir, callback = (error, exists) ->) -> + fs.lstat compileDir, (err, stats) -> + if err?.code is 'ENOENT' + return callback(null, false) # directory does not exist + else if err? + logger.err {dir: compileDir, err:err}, "error on stat of project directory for removal" + return callback(err) + else if not stats?.isDirectory() + logger.err {dir: compileDir, stats:stats}, "bad project directory for removal" + return callback new Error("project directory is not directory") else - return callback(new Error("rm -r #{compileDir} failed: #{stderr}")) + callback(null, true) # directory exists syncFromCode: (project_id, file_name, line, column, callback = (error, pdfPositions) ->) -> # If LaTeX was run in a virtual environment, the file path that synctex expects diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 018e01d2..aa9a04ab 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -99,6 +99,7 @@ describe "CompileManager", -> describe "succesfully", -> beforeEach -> @Settings.compileDir = "compiles" + @fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory: ()->true}) @proc = new EventEmitter() @proc.stdout = new EventEmitter() @proc.stderr = new EventEmitter() @@ -117,6 +118,7 @@ describe "CompileManager", -> describe "with a non-success status code", -> beforeEach -> @Settings.compileDir = "compiles" + @fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory: ()->true}) @proc = new EventEmitter() @proc.stdout = new EventEmitter() @proc.stderr = new EventEmitter() From 5d6fb4579a7309390e1c4e42cae69eb29e1fc39b Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 31 Mar 2016 11:49:15 +0100 Subject: [PATCH 089/709] remove console.log --- app/coffee/CompileManager.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index faae9da9..6f71c321 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -161,7 +161,6 @@ module.exports = CompileManager = catch err logger.err err:err, command:command, directory:directory, project_id:project_id, "error reading word count output" return callback(err) - console.log "rooooof" callback null, CompileManager._parseWordcountFromOutput(stdout) _parseWordcountFromOutput: (output) -> From 665dbff75ad04423fd77b2eb15c2165b6d9cbe24 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 31 Mar 2016 12:12:25 +0100 Subject: [PATCH 090/709] parameter check on project_id --- app.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app.coffee b/app.coffee index 7412c7f7..c7bbd8cf 100644 --- a/app.coffee +++ b/app.coffee @@ -36,6 +36,12 @@ app.use (req, res, next) -> res.setTimeout TIMEOUT next() +app.param 'project_id', (req, res, next, project_id) -> + if project_id?.match /^[a-zA-Z0-9_-]+$/ + next() + else + next new Error("invalid project id") + app.post "/project/:project_id/compile", bodyParser.json(limit: "5mb"), CompileController.compile app.delete "/project/:project_id", CompileController.clearCache From 7ff56c47933d93c7e14d290de99b38817786466e Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 31 Mar 2016 13:33:42 +0100 Subject: [PATCH 091/709] suppress error when removing nonexistent file from cache --- app/coffee/UrlCache.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index 535a7057..b72b78ca 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -87,7 +87,11 @@ module.exports = UrlCache = callback null _deleteUrlCacheFromDisk: (project_id, url, callback = (error) ->) -> - fs.unlink UrlCache._cacheFilePathForUrl(project_id, url), callback + fs.unlink UrlCache._cacheFilePathForUrl(project_id, url), (error) -> + if error? and error.code != 'ENOENT' # no error if the file isn't present + return callback(error) + else + return callback() _findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) -> db.UrlCache.find(where: { url: url, project_id: project_id }) From 24fc9391c329412453945bdb628b779112949dc6 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 31 Mar 2016 14:03:48 +0100 Subject: [PATCH 092/709] upgrade to the latest version of request --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5b1c9b6..eaa9be8c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lynx": "0.0.11", "mkdirp": "0.3.5", "mysql": "2.6.2", - "request": "~2.21.0", + "request": "^2.21.0", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.4.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", From 83e373d7e195b8bfda2364230e9fa96bd6092dbe Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 4 Apr 2016 16:22:48 +0100 Subject: [PATCH 093/709] log errors in detail when file cannot be removed --- app/coffee/ResourceWriter.coffee | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 22c8b9f7..835d45e1 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -54,9 +54,18 @@ module.exports = ResourceWriter = _deleteFileIfNotDirectory: (path, callback = (error) ->) -> fs.stat path, (error, stat) -> - return callback(error) if error? - if stat.isFile() - fs.unlink path, callback + if error? and error.code is 'ENOENT' + return callback() + else if error? + logger.err {err: error, path: path}, "error stating file in deleteFileIfNotDirectory" + return callback(error) + else if stat.isFile() + fs.unlink path, (error) -> + if error? + logger.err {err: error, path: path}, "error removing file in deleteFileIfNotDirectory" + callback(error) + else + callback() else callback() From 558e9ae22b42ed45b522989308282fcbce37551d Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 7 Apr 2016 16:16:39 +0100 Subject: [PATCH 094/709] don't log errors when files have disappeared from build directory --- app/coffee/OutputCacheManager.coffee | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 5f7558a5..aff332be 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -125,8 +125,11 @@ module.exports = OutputCacheManager = _checkFileIsSafe: (src, callback = (error, isSafe) ->) -> # check if we have a valid file to copy into the cache fs.stat src, (err, stats) -> - if err? - # some problem reading the file + if err?.code is 'ENOENT' + logger.warn err: err, file: src, "file has disappeared before copying to build cache" + callback(err, false) + else if err? + # some other problem reading the file logger.error err: err, file: src, "stat error for file in cache" callback(err, false) else if not stats.isFile() @@ -140,7 +143,10 @@ module.exports = OutputCacheManager = _copyFile: (src, dst, callback) -> # copy output file into the cache fse.copy src, dst, (err) -> - if err? + if err?.code is 'ENOENT' + logger.warn err: err, file: src, "file has disappeared when copying to build cache" + callback(err, false) + else if err? logger.error err: err, src: src, dst: dst, "copy error for file in cache" callback(err) else @@ -155,4 +161,4 @@ module.exports = OutputCacheManager = return callback(null, true) if Settings.clsi?.archive_logs and Path.basename(src) in ["output.log", "output.blg"] return callback(null, true) - return callback(null, false) \ No newline at end of file + return callback(null, false) From 3e70c0f8e4a92bbb1299988e88a6f9a0fb0815bc Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 8 Apr 2016 13:31:23 +0100 Subject: [PATCH 095/709] added example server load tcp server --- app.coffee | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app.coffee b/app.coffee index c7bbd8cf..bdc801b2 100644 --- a/app.coffee +++ b/app.coffee @@ -122,3 +122,26 @@ app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.inte setInterval () -> ProjectPersistenceManager.clearExpiredProjects() , tenMinutes = 10 * 60 * 1000 + + + +net = require('net') +os = require('os') + +fiveMinLoad = os.loadavg()[1] +availableWorkingCpus = os.cpus().length - 1 +freeLoad = availableWorkingCpus - fiveMinLoad +freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) + +server = net.createServer (socket) -> + socket.write "#{freeLoadPercentage}%\n", "ASCII" + socket.pipe socket + return + +port = 4080 + +server.listen port, -> + console.log "listening on port #{port}" + # netcat 127.0.0.1 4080 + + From 11be12fc8ec129fd6a5b421e128a4caeb59b6c4b Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 8 Apr 2016 14:14:05 +0100 Subject: [PATCH 096/709] evaluate on every call --- app.coffee | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app.coffee b/app.coffee index bdc801b2..209c9ff9 100644 --- a/app.coffee +++ b/app.coffee @@ -128,15 +128,16 @@ setInterval () -> net = require('net') os = require('os') -fiveMinLoad = os.loadavg()[1] -availableWorkingCpus = os.cpus().length - 1 -freeLoad = availableWorkingCpus - fiveMinLoad -freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) + server = net.createServer (socket) -> - socket.write "#{freeLoadPercentage}%\n", "ASCII" - socket.pipe socket - return + fiveMinLoad = os.loadavg()[1] + availableWorkingCpus = os.cpus().length - 1 + freeLoad = availableWorkingCpus - fiveMinLoad + freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) + socket.write "#{freeLoadPercentage}%\n", "ASCII" + socket.pipe socket + return port = 4080 From 84cba7365f4dcd24c614fef3ba14631e9818867f Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 8 Apr 2016 15:18:22 +0100 Subject: [PATCH 097/709] work of 1 min load and set server as up --- app.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.coffee b/app.coffee index 209c9ff9..3a5a7ca8 100644 --- a/app.coffee +++ b/app.coffee @@ -131,11 +131,11 @@ os = require('os') server = net.createServer (socket) -> - fiveMinLoad = os.loadavg()[1] + fiveMinLoad = os.loadavg()[0] availableWorkingCpus = os.cpus().length - 1 freeLoad = availableWorkingCpus - fiveMinLoad freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) - socket.write "#{freeLoadPercentage}%\n", "ASCII" + socket.write "up, #{freeLoadPercentage}%\n", "ASCII" socket.pipe socket return From 6ca8c107347047f36b9fd658729baf063aae04b9 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 8 Apr 2016 15:25:00 +0100 Subject: [PATCH 098/709] added err handler to socket --- app.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.coffee b/app.coffee index 3a5a7ca8..0a2972f3 100644 --- a/app.coffee +++ b/app.coffee @@ -136,6 +136,8 @@ server = net.createServer (socket) -> freeLoad = availableWorkingCpus - fiveMinLoad freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) socket.write "up, #{freeLoadPercentage}%\n", "ASCII" + socket.on "error", (err)-> + console.log err, "error with socket" socket.pipe socket return From 7799e0bfdd4fcbf1e56165adca2afbdbbcf60f38 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 8 Apr 2016 15:39:02 +0100 Subject: [PATCH 099/709] return 0 for server which is being hammered socket.destroy when finished --- app.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 0a2972f3..b9bf1892 100644 --- a/app.coffee +++ b/app.coffee @@ -135,10 +135,12 @@ server = net.createServer (socket) -> availableWorkingCpus = os.cpus().length - 1 freeLoad = availableWorkingCpus - fiveMinLoad freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) + if freeLoadPercentage < 0 + freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects about socket.write "up, #{freeLoadPercentage}%\n", "ASCII" socket.on "error", (err)-> console.log err, "error with socket" - socket.pipe socket + socket.destroy() return port = 4080 From cd499fa4e53f2f333dd1cce041ed70bd5150c08d Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 11 Apr 2016 13:47:06 +0100 Subject: [PATCH 100/709] server load endpoint uses settings for port --- app.coffee | 19 +++++++------------ config/settings.defaults.coffee | 1 + 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app.coffee b/app.coffee index b9bf1892..0b5c47d5 100644 --- a/app.coffee +++ b/app.coffee @@ -128,25 +128,20 @@ setInterval () -> net = require('net') os = require('os') - - server = net.createServer (socket) -> - fiveMinLoad = os.loadavg()[0] + socket.on "error", (err)-> + logger.err err:err, "error with socket on load check" + currentLoad = os.loadavg()[0] availableWorkingCpus = os.cpus().length - 1 - freeLoad = availableWorkingCpus - fiveMinLoad + freeLoad = availableWorkingCpus - currentLoad freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) if freeLoadPercentage < 0 - freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects about + freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects to different servers socket.write "up, #{freeLoadPercentage}%\n", "ASCII" - socket.on "error", (err)-> - console.log err, "error with socket" socket.destroy() - return - -port = 4080 -server.listen port, -> +server.listen port = (Settings.internal?.clsi?.load_port or 3044), -> console.log "listening on port #{port}" - # netcat 127.0.0.1 4080 + # netcat 127.0.0.1 3044 diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 8e589a55..c7d9e370 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -32,6 +32,7 @@ module.exports = internal: clsi: port: 3013 + load_port: 3044 host: "localhost" apis: From f453f954e4ce047abaf1046a6fcf45c4e7590c4a Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 12 Apr 2016 10:49:45 +0100 Subject: [PATCH 101/709] use socket.end for tcp checks --- app.coffee | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app.coffee b/app.coffee index 0b5c47d5..984f09a2 100644 --- a/app.coffee +++ b/app.coffee @@ -131,17 +131,19 @@ os = require('os') server = net.createServer (socket) -> socket.on "error", (err)-> logger.err err:err, "error with socket on load check" + socket.destroy() + currentLoad = os.loadavg()[0] availableWorkingCpus = os.cpus().length - 1 freeLoad = availableWorkingCpus - currentLoad freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) - if freeLoadPercentage < 0 + if freeLoadPercentage <= 0 freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects to different servers - socket.write "up, #{freeLoadPercentage}%\n", "ASCII" - socket.destroy() + socket.write("up, #{freeLoadPercentage}%\n", "ASCII") + socket.end() server.listen port = (Settings.internal?.clsi?.load_port or 3044), -> - console.log "listening on port #{port}" - # netcat 127.0.0.1 3044 + logger.info "tcp load endpoint listening on port #{port}" + # telnet 127.0.0.1 3044 From 3c021fd4c981b763fc8917fdfa3a9b8ab6dbd8da Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 12 Apr 2016 13:32:58 +0100 Subject: [PATCH 102/709] ignore ECONNRESET --- app.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.coffee b/app.coffee index 984f09a2..f94685aa 100644 --- a/app.coffee +++ b/app.coffee @@ -130,6 +130,9 @@ os = require('os') server = net.createServer (socket) -> socket.on "error", (err)-> + if err.code == "ECONNRESET" + # this always comes up, we don't know why + return logger.err err:err, "error with socket on load check" socket.destroy() From 6860d2be6c4bb7a0cbdadfa8bd42fe0c9b2508b4 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 13 Apr 2016 09:29:57 +0100 Subject: [PATCH 103/709] increased clsi cache to 3.5 days --- app/coffee/ProjectPersistenceManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 9f11cf31..26542b89 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -7,7 +7,7 @@ oneDay = 24 * 60 * 60 * 1000 module.exports = ProjectPersistenceManager = - EXPIRY_TIMEOUT: oneDay * 2.5 + EXPIRY_TIMEOUT: oneDay * 3.5 markProjectAsJustAccessed: (project_id, callback = (error) ->) -> db.Project.findOrCreate(where: {project_id: project_id}) From 5f7cd5ece5c5a94d1d6ecb26d314bb4015244468 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 20 Apr 2016 15:38:05 +0100 Subject: [PATCH 104/709] added project status endpoint used for getting the server a project is on --- app.coffee | 1 + app/coffee/CompileController.coffee | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app.coffee b/app.coffee index f94685aa..aaa2b7ee 100644 --- a/app.coffee +++ b/app.coffee @@ -48,6 +48,7 @@ app.delete "/project/:project_id", CompileController.clearCache app.get "/project/:project_id/sync/code", CompileController.syncFromCode app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf app.get "/project/:project_id/wordcount", CompileController.wordcount +app.get "/project/:project_id/status", CompileController.status ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks" diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index dd730409..62b1020a 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -78,3 +78,7 @@ module.exports = CompileController = res.send JSON.stringify { texcount: result } + + status: (req, res, next = (error)-> )-> + res.send("OK") + From 35240fbd4d3e19c889fcf418f776c3da4e1e5155 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 21 Apr 2016 17:40:09 +0100 Subject: [PATCH 105/709] move back to 2.5 days cache for moment --- app/coffee/ProjectPersistenceManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 26542b89..9f11cf31 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -7,7 +7,7 @@ oneDay = 24 * 60 * 60 * 1000 module.exports = ProjectPersistenceManager = - EXPIRY_TIMEOUT: oneDay * 3.5 + EXPIRY_TIMEOUT: oneDay * 2.5 markProjectAsJustAccessed: (project_id, callback = (error) ->) -> db.Project.findOrCreate(where: {project_id: project_id}) From 834668b03365c9f9d2254441cd5240f94d57aff5 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 9 May 2016 15:36:11 +0100 Subject: [PATCH 106/709] add a metric for the TeXLive image used on each compile --- app/coffee/CompileManager.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 6f71c321..ca843032 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -32,7 +32,11 @@ module.exports = CompileManager = injectDraftModeIfRequired (error) -> return callback(error) if error? timer = new Metrics.Timer("run-compile") + # find the image tag to log it as a metric + tag = request.imageName?.match(/:(.*)/)?[1] or "default" + tag = "other" if project_id?.match(/^[0-9a-f]{24}$/) # exclude smoke test Metrics.inc("compiles") + Metrics.inc("compiles-with-image.#{tag}") LatexRunner.runLatex request.project_id, { directory: compileDir mainFile: request.rootResourcePath From 0a5ca6b0fa1d2b45163139f97a3c77e9aa5ca8ca Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 9 May 2016 16:00:24 +0100 Subject: [PATCH 107/709] add timing information from /usr/bin/time --- app/coffee/CompileManager.coffee | 10 ++++++---- app/coffee/LatexRunner.coffee | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index ca843032..5c70c07e 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -43,17 +43,19 @@ module.exports = CompileManager = compiler: request.compiler timeout: request.timeout image: request.imageName - }, (error, output, stats) -> + }, (error, output, stats, timings) -> return callback(error) if error? Metrics.inc("compiles-succeeded") for metric_key, metric_value of stats or {} Metrics.count(metric_key, metric_value) + for metric_key, metric_value of timings or {} + Metrics.timing(metric_key, metric_value) loadavg = os.loadavg?() Metrics.gauge("load-avg", loadavg[0]) if loadavg? ts = timer.done() - logger.log {project_id: request.project_id, time_taken: ts, stats:stats, loadavg:loadavg}, "done compile" - if stats?["latex-runs"] > 0 - Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"]) + logger.log {project_id: request.project_id, time_taken: ts, stats:stats, timings:timings, loadavg:loadavg}, "done compile" + if stats?["latex-runs"] > 0 and timings?["cpu-time"] > 0 + Metrics.timing("run-compile-per-pass", timings["cpu-time"] / stats["latex-runs"]) OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> return callback(error) if error? diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 4280d95a..e5861a5c 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -41,9 +41,13 @@ module.exports = LatexRunner = stats["latex-runs-with-errors"] = if failed then runs else 0 stats["latex-runs-#{runs}"] = 1 stats["latex-runs-with-errors-#{runs}"] = if failed then 1 else 0 - callback error, output, stats + # timing information from /usr/bin/time + timings = {} + timings["cpu-percent"] = output?.stderr?.match(/Percent of CPU this job got: (\d+)/m)?[1] or 0 + timings["cpu-time"] = output?.stderr?.match(/User time.*: (\d+.\d+)/m)?[1] or 0 + callback error, output, stats, timings - _latexmkBaseCommand: ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] + _latexmkBaseCommand: ["/usr/bin/time", "-v", "latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR"] _pdflatexCommand: (mainFile) -> LatexRunner._latexmkBaseCommand.concat [ From e70bd3ae8e9a2a6e56b6c06bbb4f2b05d74155da Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 10 May 2016 09:12:00 +0100 Subject: [PATCH 108/709] preserve existing metric name --- app/coffee/CompileManager.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 5c70c07e..dc6c8337 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -54,8 +54,10 @@ module.exports = CompileManager = Metrics.gauge("load-avg", loadavg[0]) if loadavg? ts = timer.done() logger.log {project_id: request.project_id, time_taken: ts, stats:stats, timings:timings, loadavg:loadavg}, "done compile" + if stats?["latex-runs"] > 0 + Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"]) if stats?["latex-runs"] > 0 and timings?["cpu-time"] > 0 - Metrics.timing("run-compile-per-pass", timings["cpu-time"] / stats["latex-runs"]) + Metrics.timing("run-compile-cpu-time-per-pass", timings["cpu-time"] / stats["latex-runs"]) OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> return callback(error) if error? From dfd2bc31ef7ed2c5c2cf6b9a6d1fce58a36f5353 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 10 May 2016 09:12:13 +0100 Subject: [PATCH 109/709] record system time --- app/coffee/LatexRunner.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index e5861a5c..65a30464 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -43,8 +43,10 @@ module.exports = LatexRunner = stats["latex-runs-with-errors-#{runs}"] = if failed then 1 else 0 # timing information from /usr/bin/time timings = {} - timings["cpu-percent"] = output?.stderr?.match(/Percent of CPU this job got: (\d+)/m)?[1] or 0 - timings["cpu-time"] = output?.stderr?.match(/User time.*: (\d+.\d+)/m)?[1] or 0 + stderr = output?.stderr + timings["cpu-percent"] = stderr?.match(/Percent of CPU this job got: (\d+)/m)?[1] or 0 + timings["cpu-time"] = stderr?.match(/User time.*: (\d+.\d+)/m)?[1] or 0 + 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"] From 143913c67f36bd9354eeb3e67aca57b4182bb4f3 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 10 May 2016 09:41:39 +0100 Subject: [PATCH 110/709] fix tagname for graphite --- app/coffee/CompileManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index dc6c8337..dcd1a98b 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -32,8 +32,8 @@ module.exports = CompileManager = injectDraftModeIfRequired (error) -> return callback(error) if error? timer = new Metrics.Timer("run-compile") - # find the image tag to log it as a metric - tag = request.imageName?.match(/:(.*)/)?[1] or "default" + # find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) + tag = request.imageName?.match(/:(.*)/)?[1]?.replace(/\./g,'-') or "default" tag = "other" if project_id?.match(/^[0-9a-f]{24}$/) # exclude smoke test Metrics.inc("compiles") Metrics.inc("compiles-with-image.#{tag}") From 51f87c5f795cc22f6a5ff41ffa040de797e8494c Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 10 May 2016 10:10:01 +0100 Subject: [PATCH 111/709] fix logic excluding smoke test in metric --- app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index dcd1a98b..54a6a7eb 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -34,7 +34,7 @@ module.exports = CompileManager = timer = new Metrics.Timer("run-compile") # find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) tag = request.imageName?.match(/:(.*)/)?[1]?.replace(/\./g,'-') or "default" - tag = "other" if project_id?.match(/^[0-9a-f]{24}$/) # exclude smoke test + tag = "other" if not request.project_id.match(/^[0-9a-f]{24}$/) # exclude smoke test Metrics.inc("compiles") Metrics.inc("compiles-with-image.#{tag}") LatexRunner.runLatex request.project_id, { From f92c70935bd29ce4fa7671952645a37d0da3a281 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 13 May 2016 10:10:48 +0100 Subject: [PATCH 112/709] allow direct path to output file /project/project_id/build/build_id/output/* this avoids use of the query string ?build=... and so we can match the url directly with the nginx location directive --- app.coffee | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app.coffee b/app.coffee index aaa2b7ee..275c6bf0 100644 --- a/app.coffee +++ b/app.coffee @@ -42,6 +42,13 @@ app.param 'project_id', (req, res, next, project_id) -> else next new Error("invalid project id") +app.param 'build_id', (req, res, next, build_id) -> + if build_id?.match OutputCacheManager.BUILD_REGEX + next() + else + next new Error("invalid build id #{build_id}") + + app.post "/project/:project_id/compile", bodyParser.json(limit: "5mb"), CompileController.compile app.delete "/project/:project_id", CompileController.clearCache @@ -65,6 +72,11 @@ staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHead res.set("Etag", etag(path, stat)) res.set("Content-Type", ContentTypeMapper.map(path)) +app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) -> + # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}") + staticServer(req, res, next) + app.get "/project/:project_id/output/*", (req, res, next) -> if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX) # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) From 434e00cb7444559dc5670487cd2661b59e1a9f74 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 13 May 2016 10:11:35 +0100 Subject: [PATCH 113/709] make the build id a secure random token we allow existing build ids to work for backwards compatibility this can be removed after some time --- app/coffee/OutputCacheManager.coffee | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index aff332be..7f11bc89 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -5,13 +5,16 @@ Path = require "path" logger = require "logger-sharelatex" _ = require "underscore" Settings = require "settings-sharelatex" +crypto = require "crypto" OutputFileOptimiser = require "./OutputFileOptimiser" module.exports = OutputCacheManager = CACHE_SUBDIR: '.cache/clsi' ARCHIVE_SUBDIR: '.archive/clsi' - BUILD_REGEX: /^[0-9a-f]+$/ # build id is Date.now() converted to hex + # build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes + # for backwards compatibility, make the randombytes part optional + BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/ CACHE_LIMIT: 2 # maximum number of cache directories CACHE_AGE: 60*60*1000 # up to one hour old @@ -23,12 +26,24 @@ module.exports = OutputCacheManager = # for invalid build id, return top level return file + generateBuildId: (callback = (error, buildId) ->) -> + # generate a secure build id from Date.now() and 8 random bytes in hex + crypto.randomBytes 8, (err, buf) -> + return callback(err) if err? + random = buf.toString('hex') + date = Date.now().toString(16) + callback err, "#{date}-#{random}" + saveOutputFiles: (outputFiles, compileDir, callback = (error) ->) -> + OutputCacheManager.generateBuildId (err, buildId) -> + return callback(err) if err? + OutputCacheManager.saveOutputFilesInBuildDir outputFiles, compileDir, buildId, callback + + saveOutputFilesInBuildDir: (outputFiles, compileDir, buildId, callback = (error) ->) -> # make a compileDir/CACHE_SUBDIR/build_id directory and # copy all the output files into it cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR) # Put the files into a new cache subdirectory - buildId = Date.now().toString(16) cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId) # let file expiry run in the background @@ -36,7 +51,7 @@ module.exports = OutputCacheManager = # Archive logs in background if Settings.clsi?.archive_logs or Settings.clsi?.strace - OutputCacheManager.archiveLogs outputFiles, compileDir, (err) -> + OutputCacheManager.archiveLogs outputFiles, compileDir, buildId, (err) -> if err? logger.warn err:err, "erroring archiving log files" @@ -71,9 +86,8 @@ module.exports = OutputCacheManager = else # pass back the list of new files in the cache callback(err, results) - - archiveLogs: (outputFiles, compileDir, callback = (error) ->) -> - buildId = Date.now().toString(16) + + archiveLogs: (outputFiles, compileDir, buildId, callback = (error) ->) -> archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId) logger.log {dir: archiveDir}, "archiving log files for project" fse.ensureDir archiveDir, (err) -> @@ -104,8 +118,9 @@ module.exports = OutputCacheManager = return false if options?.keep == dir # remove any directories over the hard limit return true if index > OutputCacheManager.CACHE_LIMIT - # we can get the build time from the directory name - dirTime = parseInt(dir, 16) + # we can get the build time from the first part of the directory name DDDD-RRRR + # DDDD is date and RRRR is random bytes + dirTime = parseInt(dir.split('-')?[0], 16) age = currentTime - dirTime return age > OutputCacheManager.CACHE_AGE From d26c6b933e916b898a79d5055b9945da80c1d280 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 19 May 2016 16:23:07 +0100 Subject: [PATCH 114/709] return the file path in the output file list for easy lookup --- app/coffee/CompileController.coffee | 1 + test/unit/coffee/CompileControllerTests.coffee | 1 + 2 files changed, 2 insertions(+) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 62b1020a..5cc9a5e2 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -34,6 +34,7 @@ module.exports = CompileController = error: error?.message or error outputFiles: outputFiles.map (file) -> url: "#{Settings.apis.clsi.url}/project/#{request.project_id}/output/#{file.path}" + path: file.path type: file.type build: file.build } diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index 6a18ce79..d8d6e862 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -76,6 +76,7 @@ describe "CompileController", -> error: null outputFiles: @output_files.map (file) => url: "#{@Settings.apis.clsi.url}/project/#{@project_id}/output/#{file.path}" + path: file.path type: file.type build: file.build ) From f8ae215c1e9decc0819b0aa961f16e3f33aa4423 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 19 May 2016 16:23:33 +0100 Subject: [PATCH 115/709] avoid clobbering the existing port variable --- app.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.coffee b/app.coffee index 275c6bf0..2a8e7b5f 100644 --- a/app.coffee +++ b/app.coffee @@ -158,8 +158,8 @@ server = net.createServer (socket) -> socket.write("up, #{freeLoadPercentage}%\n", "ASCII") socket.end() -server.listen port = (Settings.internal?.clsi?.load_port or 3044), -> - logger.info "tcp load endpoint listening on port #{port}" +server.listen load_port = (Settings.internal?.clsi?.load_port or 3044), -> + logger.info "tcp load endpoint listening on port #{load_port}" # telnet 127.0.0.1 3044 From a2c2fc3a51de4c9ec90cc65faf61042a26a26fb6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 19 May 2016 16:51:50 +0100 Subject: [PATCH 116/709] make cached assets ttl set via config --- app/coffee/ProjectPersistenceManager.coffee | 4 +++- config/settings.defaults.coffee | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 9f11cf31..b716d739 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -4,10 +4,11 @@ db = require "./db" async = require "async" logger = require "logger-sharelatex" oneDay = 24 * 60 * 60 * 1000 +Settings = require "settings-sharelatex" module.exports = ProjectPersistenceManager = - EXPIRY_TIMEOUT: oneDay * 2.5 + EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5 markProjectAsJustAccessed: (project_id, callback = (error) ->) -> db.Project.findOrCreate(where: {project_id: project_id}) @@ -52,3 +53,4 @@ module.exports = ProjectPersistenceManager = .then((projects) -> callback null, projects.map((project) -> project.project_id) ).error callback + diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index c7d9e370..aa5780de 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -35,8 +35,10 @@ module.exports = load_port: 3044 host: "localhost" + apis: clsi: url: "http://localhost:3013" smokeTest: false + project_cache_length_ms: 60 * 60 * 24 \ No newline at end of file From 6b107bd20a0f49efe40304fc701d74997a2fbd40 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 19 May 2016 16:57:14 +0100 Subject: [PATCH 117/709] log out EXPIRY_TIMEOUT --- app/coffee/ProjectPersistenceManager.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index b716d739..f649f204 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -54,3 +54,5 @@ module.exports = ProjectPersistenceManager = callback null, projects.map((project) -> project.project_id) ).error callback + +logger.log EXPIRY_TIMEOUT:EXPIRY_TIMEOUT, "project assets kept timeout" \ No newline at end of file From 3379577499516b6838fb903f69fd15d96c44c2f7 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 20 May 2016 10:22:45 +0100 Subject: [PATCH 118/709] fix error in log for expiry timeout --- app/coffee/ProjectPersistenceManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index f649f204..f70f43ca 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -55,4 +55,4 @@ module.exports = ProjectPersistenceManager = ).error callback -logger.log EXPIRY_TIMEOUT:EXPIRY_TIMEOUT, "project assets kept timeout" \ No newline at end of file +logger.log {EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout" From 0da85d5d03272be4f4d5b6ec827ace125bd706af Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 19 May 2016 16:40:12 +0100 Subject: [PATCH 119/709] be ready to serve files from per-user containers --- app.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app.coffee b/app.coffee index 2a8e7b5f..9f54f4a0 100644 --- a/app.coffee +++ b/app.coffee @@ -42,6 +42,12 @@ app.param 'project_id', (req, res, next, project_id) -> else next new Error("invalid project id") +app.param 'user_id', (req, res, next, project_id) -> + if project_id?.match /^[a-zA-Z0-9_-]+$/ + next() + else + next new Error("invalid user id") + app.param 'build_id', (req, res, next, build_id) -> if build_id?.match OutputCacheManager.BUILD_REGEX next() @@ -72,6 +78,11 @@ staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHead res.set("Etag", etag(path, stat)) res.set("Content-Type", ContentTypeMapper.map(path)) +app.get "/project/:project_id/user/:user_id/build/:build_id/output/*", (req, res, next) -> + # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = "/#{req.params.project_id}-#{req.params.user_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}") + staticServer(req, res, next) + app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) -> # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}") From 2e97bcba3a46b2273511be6f1ff1aaeca8d4e2c8 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 23 May 2016 14:13:55 +0100 Subject: [PATCH 120/709] add error handler on CommandRunner --- app/coffee/CommandRunner.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 41cbfaa8..842eb9db 100644 --- a/app/coffee/CommandRunner.coffee +++ b/app/coffee/CommandRunner.coffee @@ -8,5 +8,8 @@ module.exports = CommandRunner = logger.warn "timeouts and sandboxing are not enabled with CommandRunner" proc = spawn command[0], command.slice(1), stdio: "inherit", cwd: directory + 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 From 22f730c3e9bf65238cb97b44251709b053feddf6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 23 May 2016 14:31:27 +0100 Subject: [PATCH 121/709] parallelFileDownloads defaults to 1, sql can't take it --- app/coffee/ResourceWriter.coffee | 5 ++++- config/settings.defaults.coffee | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 835d45e1..e3639006 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -6,6 +6,9 @@ mkdirp = require "mkdirp" OutputFileFinder = require "./OutputFileFinder" Metrics = require "./Metrics" logger = require "logger-sharelatex" +settings = require("settings-sharelatex") + +parallelFileDownloads = settings.parallelFileDownloads or 1 module.exports = ResourceWriter = syncResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @@ -16,7 +19,7 @@ module.exports = ResourceWriter = jobs = for resource in resources do (resource) => (callback) => @_writeResourceToDisk(project_id, resource, basePath, callback) - async.parallelLimit jobs, 5, callback + async.parallelLimit jobs, parallelFileDownloads, callback _createDirectory: (basePath, callback = (error) ->) -> fs.mkdir basePath, (err) -> diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index aa5780de..ae8e1323 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -41,4 +41,5 @@ module.exports = url: "http://localhost:3013" smokeTest: false - project_cache_length_ms: 60 * 60 * 24 \ No newline at end of file + project_cache_length_ms: 60 * 60 * 24 + parallelFileDownloads:1 \ No newline at end of file From 2c3b1126b0225fd772db79709c2c7140dab0d3e2 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 23 May 2016 15:45:39 +0100 Subject: [PATCH 122/709] log out if the command running is being used --- app/coffee/CommandRunner.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 842eb9db..5ea6765c 100644 --- a/app/coffee/CommandRunner.coffee +++ b/app/coffee/CommandRunner.coffee @@ -1,6 +1,8 @@ spawn = require("child_process").spawn logger = require "logger-sharelatex" +logger.info "using standard command runner" + module.exports = CommandRunner = run: (project_id, command, directory, image, timeout, callback = (error) ->) -> command = (arg.replace('$COMPILE_DIR', directory) for arg in command) From b2f687c061ffeaffd6bec50736c48b335dd6152e Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 24 May 2016 14:08:39 +0100 Subject: [PATCH 123/709] log out which command logger is used --- app/coffee/CompileManager.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 54a6a7eb..ee2c4077 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -7,11 +7,14 @@ Path = require "path" logger = require "logger-sharelatex" Metrics = require "./Metrics" child_process = require "child_process" -CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner") DraftModeManager = require "./DraftModeManager" fs = require("fs") os = require("os") +commandRunner = Settings.clsi?.commandRunner or "./CommandRunner" +logger.info commandRunner:commandRunner, "selecting command runner for clsi" +CommandRunner = require(commandRunner) + module.exports = CompileManager = doCompile: (request, callback = (error, outputFiles) ->) -> compileDir = Path.join(Settings.path.compilesDir, request.project_id) From da324a8dd06ae0ca9acc8cc423a4eaee1edb45f5 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 24 May 2016 14:12:02 +0100 Subject: [PATCH 124/709] added logger.info to test setup --- test/unit/coffee/CompileManagerTests.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index aa9a04ab..55f5cc52 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -14,7 +14,7 @@ describe "CompileManager", -> "./OutputFileFinder": @OutputFileFinder = {} "./OutputCacheManager": @OutputCacheManager = {} "settings-sharelatex": @Settings = { path: compilesDir: "/compiles/dir" } - "logger-sharelatex": @logger = { log: sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub() , info:->} "child_process": @child_process = {} "./CommandRunner": @CommandRunner = {} "./DraftModeManager": @DraftModeManager = {} From cda1e301f68b9fb17c61eab8dccadf4e294075fb Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 27 May 2016 14:45:39 +0100 Subject: [PATCH 125/709] log out errors more clearly --- app/coffee/CompileManager.coffee | 4 +++- app/coffee/OutputFileFinder.coffee | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index ee2c4077..d119cf66 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -22,7 +22,9 @@ module.exports = CompileManager = timer = new Metrics.Timer("write-to-disk") logger.log project_id: request.project_id, "starting compile" ResourceWriter.syncResourcesToDisk request.project_id, request.resources, compileDir, (error) -> - return callback(error) if error? + if error? + logger.err err:error, project_id: request.project_id, "error writing resources to disk" + return callback(error) logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "written files to disk" timer.done() diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index b4a91625..c89cc8ad 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -13,7 +13,9 @@ module.exports = OutputFileFinder = logger.log directory: directory, "getting output files" OutputFileFinder._getAllFiles directory, (error, allFiles = []) -> - return callback(error) if error? + if error? + logger.err err:error, "error finding all output files" + return callback(error) jobs = [] outputFiles = [] for file in allFiles From ac3b7a571a1decebab4d2f6849ba708b42d42450 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 27 May 2016 16:18:18 +0100 Subject: [PATCH 126/709] log out error on synctex --- app/coffee/CompileManager.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index d119cf66..41377aba 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -20,7 +20,7 @@ module.exports = CompileManager = compileDir = Path.join(Settings.path.compilesDir, request.project_id) timer = new Metrics.Timer("write-to-disk") - logger.log project_id: request.project_id, "starting compile" + logger.log project_id: request.project_id, "starting doCompile" ResourceWriter.syncResourcesToDisk request.project_id, request.resources, compileDir, (error) -> if error? logger.err err:error, project_id: request.project_id, "error writing resources to disk" @@ -130,7 +130,9 @@ module.exports = CompileManager = bin_path = Path.resolve(__dirname + "/../../bin/synctex") seconds = 1000 child_process.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) -> - return callback(error) if error? + if error? + logger.err err:error, args:args, "error running synctex" + return callback(error) callback(null, stdout) _parseSynctexFromCodeOutput: (output) -> From 78b88683fc22ad2a5e4e5cbf2285fe132df30ef6 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 27 May 2016 15:25:13 +0100 Subject: [PATCH 127/709] put the build id in the output file urls the url attribute will now give the preferred location for accessing the output file, without the url having to be constructed by the web client --- app/coffee/CompileController.coffee | 5 ++++- test/unit/coffee/CompileControllerTests.coffee | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 5cc9a5e2..478d91bf 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -33,7 +33,10 @@ module.exports = CompileController = status: status error: error?.message or error outputFiles: outputFiles.map (file) -> - url: "#{Settings.apis.clsi.url}/project/#{request.project_id}/output/#{file.path}" + url: + "#{Settings.apis.clsi.url}/project/#{request.project_id}" + + (if file.build? then "/build/#{file.build}" else "") + + "/output/#{file.path}" path: file.path type: file.type build: file.build diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index d8d6e862..7693799e 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -75,7 +75,7 @@ describe "CompileController", -> status: "success" error: null outputFiles: @output_files.map (file) => - url: "#{@Settings.apis.clsi.url}/project/#{@project_id}/output/#{file.path}" + url: "#{@Settings.apis.clsi.url}/project/#{@project_id}/build/#{file.build}/output/#{file.path}" path: file.path type: file.type build: file.build From 8c42a353e16f31cf088c20878f2ccd7b7ccc28c8 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 27 May 2016 15:25:13 +0100 Subject: [PATCH 128/709] put the build id in the output file urls the url attribute will now give the preferred location for accessing the output file, without the url having to be constructed by the web client --- app/coffee/CompileController.coffee | 5 ++++- test/unit/coffee/CompileControllerTests.coffee | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 5cc9a5e2..478d91bf 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -33,7 +33,10 @@ module.exports = CompileController = status: status error: error?.message or error outputFiles: outputFiles.map (file) -> - url: "#{Settings.apis.clsi.url}/project/#{request.project_id}/output/#{file.path}" + url: + "#{Settings.apis.clsi.url}/project/#{request.project_id}" + + (if file.build? then "/build/#{file.build}" else "") + + "/output/#{file.path}" path: file.path type: file.type build: file.build diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index d8d6e862..7693799e 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -75,7 +75,7 @@ describe "CompileController", -> status: "success" error: null outputFiles: @output_files.map (file) => - url: "#{@Settings.apis.clsi.url}/project/#{@project_id}/output/#{file.path}" + url: "#{@Settings.apis.clsi.url}/project/#{@project_id}/build/#{file.build}/output/#{file.path}" path: file.path type: file.type build: file.build From 226e6c87b1b06c7da505700e667b4506a37c8947 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 27 May 2016 15:29:26 +0100 Subject: [PATCH 129/709] add per-user routes and methods --- app.coffee | 12 ++++- app/coffee/CompileController.coffee | 11 ++-- app/coffee/CompileManager.coffee | 50 ++++++++++++------- .../unit/coffee/CompileControllerTests.coffee | 12 ++--- test/unit/coffee/CompileManagerTests.coffee | 25 +++++----- 5 files changed, 68 insertions(+), 42 deletions(-) diff --git a/app.coffee b/app.coffee index 9f54f4a0..cfe8ebcc 100644 --- a/app.coffee +++ b/app.coffee @@ -42,8 +42,8 @@ app.param 'project_id', (req, res, next, project_id) -> else next new Error("invalid project id") -app.param 'user_id', (req, res, next, project_id) -> - if project_id?.match /^[a-zA-Z0-9_-]+$/ +app.param 'user_id', (req, res, next, user_id) -> + if user_id?.match /^[0-9a-f]{24}$/ next() else next new Error("invalid user id") @@ -63,6 +63,14 @@ app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf app.get "/project/:project_id/wordcount", CompileController.wordcount 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.delete "/project/:project_id/user/:user_id", CompileController.clearCache + +app.get "/project/:project_id/user/:user_id/sync/code", CompileController.syncFromCode +app.get "/project/:project_id/user/:user_id/sync/pdf", CompileController.syncFromPdf +app.get "/project/:project_id/user/:user_id/wordcount", CompileController.wordcount + ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks" # create a static server which does not allow access to any symlinks diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 478d91bf..7e1f0789 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -11,6 +11,7 @@ module.exports = CompileController = RequestParser.parse req.body, (error, request) -> return next(error) if error? request.project_id = req.params.project_id + request.user_id = req.params.user_id if req.params.user_id? ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? CompileManager.doCompile request, (error, outputFiles = []) -> @@ -35,6 +36,7 @@ module.exports = CompileController = outputFiles: outputFiles.map (file) -> url: "#{Settings.apis.clsi.url}/project/#{request.project_id}" + + (if request.user_id? then "/user/#{request.user_id}" else "") + (if file.build? then "/build/#{file.build}" else "") + "/output/#{file.path}" path: file.path @@ -52,8 +54,9 @@ module.exports = CompileController = line = parseInt(req.query.line, 10) column = parseInt(req.query.column, 10) project_id = req.params.project_id + user_id = req.params.user_id - CompileManager.syncFromCode project_id, file, line, column, (error, pdfPositions) -> + CompileManager.syncFromCode project_id, user_id, file, line, column, (error, pdfPositions) -> return next(error) if error? res.send JSON.stringify { pdf: pdfPositions @@ -64,8 +67,9 @@ module.exports = CompileController = h = parseFloat(req.query.h) v = parseFloat(req.query.v) project_id = req.params.project_id + user_id = req.params.user_id - CompileManager.syncFromPdf project_id, page, h, v, (error, codePositions) -> + CompileManager.syncFromPdf project_id, user_id, page, h, v, (error, codePositions) -> return next(error) if error? res.send JSON.stringify { code: codePositions @@ -74,10 +78,11 @@ module.exports = CompileController = wordcount: (req, res, next = (error) ->) -> file = req.query.file || "main.tex" project_id = req.params.project_id + user_id = req.params.user_id image = req.query.image logger.log {image, file, project_id}, "word count request" - CompileManager.wordcount project_id, file, image, (error, result) -> + CompileManager.wordcount project_id, user_id, file, image, (error, result) -> return next(error) if error? res.send JSON.stringify { texcount: result diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 41377aba..cd332a4f 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -15,17 +15,23 @@ commandRunner = Settings.clsi?.commandRunner or "./CommandRunner" logger.info commandRunner:commandRunner, "selecting command runner for clsi" CommandRunner = require(commandRunner) +getCompileName = (project_id, user_id) -> + if user_id? then "#{project_id}-#{user_id}" else project_id + +getCompileDir = (project_id, user_id) -> + Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id)) + module.exports = CompileManager = doCompile: (request, callback = (error, outputFiles) ->) -> - compileDir = Path.join(Settings.path.compilesDir, request.project_id) + compileDir = getCompileDir(request.project_id, request.user_id) timer = new Metrics.Timer("write-to-disk") - logger.log project_id: request.project_id, "starting doCompile" + logger.log project_id: request.project_id, user_id: request.user_id, "starting compile" ResourceWriter.syncResourcesToDisk request.project_id, request.resources, compileDir, (error) -> if error? - logger.err err:error, project_id: request.project_id, "error writing resources to disk" + logger.err err:error, project_id: request.project_id, user_id: request.user_id, "error writing resources to disk" return callback(error) - logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "written files to disk" + logger.log project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start, "written files to disk" timer.done() injectDraftModeIfRequired = (callback) -> @@ -42,7 +48,8 @@ module.exports = CompileManager = tag = "other" if not request.project_id.match(/^[0-9a-f]{24}$/) # exclude smoke test Metrics.inc("compiles") Metrics.inc("compiles-with-image.#{tag}") - LatexRunner.runLatex request.project_id, { + compileName = getCompileName(request.project_id, request.user_id) + LatexRunner.runLatex compileName, { directory: compileDir mainFile: request.rootResourcePath compiler: request.compiler @@ -58,7 +65,7 @@ module.exports = CompileManager = loadavg = os.loadavg?() Metrics.gauge("load-avg", loadavg[0]) if loadavg? ts = timer.done() - logger.log {project_id: request.project_id, time_taken: ts, stats:stats, timings:timings, loadavg:loadavg}, "done compile" + logger.log {project_id: request.project_id, user_id: request.user_id, time_taken: ts, stats:stats, timings:timings, loadavg:loadavg}, "done compile" if stats?["latex-runs"] > 0 Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"]) if stats?["latex-runs"] > 0 and timings?["cpu-time"] > 0 @@ -106,24 +113,28 @@ module.exports = CompileManager = else callback(null, true) # directory exists - syncFromCode: (project_id, file_name, line, column, callback = (error, pdfPositions) ->) -> + syncFromCode: (project_id, user_id, file_name, line, column, callback = (error, pdfPositions) ->) -> # If LaTeX was run in a virtual environment, the file path that synctex expects # might not match the file path on the host. The .synctex.gz file however, will be accessed # wherever it is on the host. - base_dir = Settings.path.synctexBaseDir(project_id) + compileName = getCompileName(project_id, user_id) + base_dir = Settings.path.synctexBaseDir(compileName) file_path = base_dir + "/" + file_name - synctex_path = Path.join(Settings.path.compilesDir, project_id, "output.pdf") + compileDir = getCompileDir(project_id, user_id) + synctex_path = Path.join(compileDir, "output.pdf") CompileManager._runSynctex ["code", synctex_path, file_path, line, column], (error, stdout) -> return callback(error) if error? - logger.log project_id: project_id, file_name: file_name, line: line, column: column, stdout: stdout, "synctex code output" + logger.log project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, stdout: stdout, "synctex code output" callback null, CompileManager._parseSynctexFromCodeOutput(stdout) - syncFromPdf: (project_id, page, h, v, callback = (error, filePositions) ->) -> - base_dir = Settings.path.synctexBaseDir(project_id) - synctex_path = Path.join(Settings.path.compilesDir, project_id, "output.pdf") + syncFromPdf: (project_id, user_id, page, h, v, callback = (error, filePositions) ->) -> + compileName = getCompileName(project_id, user_id) + base_dir = Settings.path.synctexBaseDir(compileName) + compileDir = getCompileDir(project_id, user_id) + synctex_path = Path.join(compileDir, "output.pdf") CompileManager._runSynctex ["pdf", synctex_path, page, h, v], (error, stdout) -> return callback(error) if error? - logger.log project_id: project_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" + logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) _runSynctex: (args, callback = (error, stdout) ->) -> @@ -162,19 +173,20 @@ module.exports = CompileManager = } return results - wordcount: (project_id, file_name, image, callback = (error, pdfPositions) ->) -> - logger.log project_id:project_id, file_name:file_name, image:image, "running wordcount" + 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"] - directory = Path.join(Settings.path.compilesDir, project_id) + directory = getCompileDir(project_id, user_id) timeout = 10 * 1000 + compileName = getCompileName(project_id, user_id) - CommandRunner.run project_id, 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") catch err - logger.err err:err, command:command, directory:directory, project_id:project_id, "error reading word count output" + 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) diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index 7693799e..1fc6a99b 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -146,12 +146,12 @@ describe "CompileController", -> column: @column.toString() @res.send = sinon.stub() - @CompileManager.syncFromCode = sinon.stub().callsArgWith(4, null, @pdfPositions = ["mock-positions"]) + @CompileManager.syncFromCode = sinon.stub().callsArgWith(5, null, @pdfPositions = ["mock-positions"]) @CompileController.syncFromCode @req, @res, @next it "should find the corresponding location in the PDF", -> @CompileManager.syncFromCode - .calledWith(@project_id, @file, @line, @column) + .calledWith(@project_id, undefined, @file, @line, @column) .should.equal true it "should return the positions", -> @@ -175,12 +175,12 @@ describe "CompileController", -> v: @v.toString() @res.send = sinon.stub() - @CompileManager.syncFromPdf = sinon.stub().callsArgWith(4, null, @codePositions = ["mock-positions"]) + @CompileManager.syncFromPdf = sinon.stub().callsArgWith(5, null, @codePositions = ["mock-positions"]) @CompileController.syncFromPdf @req, @res, @next it "should find the corresponding location in the code", -> @CompileManager.syncFromPdf - .calledWith(@project_id, @page, @h, @v) + .calledWith(@project_id, undefined, @page, @h, @v) .should.equal true it "should return the positions", -> @@ -201,12 +201,12 @@ describe "CompileController", -> image: @image = "example.com/image" @res.send = sinon.stub() - @CompileManager.wordcount = sinon.stub().callsArgWith(3, null, @texcount = ["mock-texcount"]) + @CompileManager.wordcount = sinon.stub().callsArgWith(4, null, @texcount = ["mock-texcount"]) @CompileController.wordcount @req, @res, @next it "should return the word count of a file", -> @CompileManager.wordcount - .calledWith(@project_id, @file, @image) + .calledWith(@project_id, undefined, @file, @image) .should.equal true it "should return the texcount info", -> diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 55f5cc52..0ba62b95 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -43,11 +43,12 @@ describe "CompileManager", -> resources: @resources = "mock-resources" rootResourcePath: @rootResourcePath = "main.tex" project_id: @project_id = "project-id-123" + user_id: @user_id = "1234" compiler: @compiler = "pdflatex" timeout: @timeout = 42000 imageName: @image = "example.com/image" @Settings.compileDir = "compiles" - @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}" + @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArg(3) @LatexRunner.runLatex = sinon.stub().callsArg(2) @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @@ -65,7 +66,7 @@ describe "CompileManager", -> it "should run LaTeX", -> @LatexRunner.runLatex - .calledWith(@project_id, { + .calledWith("#{@project_id}-#{@user_id}", { directory: @compileDir mainFile: @rootResourcePath compiler: @compiler @@ -150,17 +151,17 @@ describe "CompileManager", -> @column = 3 @file_name = "main.tex" @child_process.execFile = sinon.stub() - @Settings.path.synctexBaseDir = (project_id) => "#{@Settings.path.compilesDir}/#{@project_id}" + @Settings.path.synctexBaseDir = (project_id) => "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" describe "syncFromCode", -> beforeEach -> @child_process.execFile.callsArgWith(3, null, @stdout = "NODE\t#{@page}\t#{@h}\t#{@v}\t#{@width}\t#{@height}\n", "") - @CompileManager.syncFromCode @project_id, @file_name, @line, @column, @callback + @CompileManager.syncFromCode @project_id, @user_id, @file_name, @line, @column, @callback it "should execute the synctex binary", -> bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}/output.pdf" - file_path = "#{@Settings.path.compilesDir}/#{@project_id}/#{@file_name}" + synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" + file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}" @child_process.execFile .calledWith(bin_path, ["code", synctex_path, file_path, @line, @column], timeout: 10000) .should.equal true @@ -178,12 +179,12 @@ describe "CompileManager", -> describe "syncFromPdf", -> beforeEach -> - @child_process.execFile.callsArgWith(3, null, @stdout = "NODE\t#{@Settings.path.compilesDir}/#{@project_id}/#{@file_name}\t#{@line}\t#{@column}\n", "") - @CompileManager.syncFromPdf @project_id, @page, @h, @v, @callback + @child_process.execFile.callsArgWith(3, null, @stdout = "NODE\t#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}\t#{@line}\t#{@column}\n", "") + @CompileManager.syncFromPdf @project_id, @user_id, @page, @h, @v, @callback it "should execute the synctex binary", -> bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}/output.pdf" + synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" @child_process.execFile .calledWith(bin_path, ["pdf", synctex_path, @page, @h, @v], timeout: 10000) .should.equal true @@ -209,15 +210,15 @@ describe "CompileManager", -> @Settings.path.compilesDir = "/local/compile/directory" @image = "example.com/image" - @CompileManager.wordcount @project_id, @file_name, @image, @callback + @CompileManager.wordcount @project_id, @user_id, @file_name, @image, @callback it "should run the texcount command", -> - @directory = "#{@Settings.path.compilesDir}/#{@project_id}" + @directory = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @file_path = "$COMPILE_DIR/#{@file_name}" @command =[ "texcount", "-inc", @file_path, "-out=" + @file_path + ".wc"] @CommandRunner.run - .calledWith(@project_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", -> From 0887fe3a7289b474ee12d5e421aa09c70137ab1d Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 27 May 2016 15:31:44 +0100 Subject: [PATCH 130/709] add per-user routes for clearing cache and extend expiry methods this adds separate functionality for clearing the cache (assets and database) and the project compile directory for a specific user --- app.coffee | 2 +- app/coffee/CompileController.coffee | 2 +- app/coffee/CompileManager.coffee | 27 ++++++++++++++++-- app/coffee/ProjectPersistenceManager.coffee | 28 ++++++++++++------- test/unit/coffee/CompileManagerTests.coffee | 10 +++---- .../ProjectPersistenceManagerTests.coffee | 12 ++++---- 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/app.coffee b/app.coffee index cfe8ebcc..6a8d73fe 100644 --- a/app.coffee +++ b/app.coffee @@ -65,7 +65,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.delete "/project/:project_id/user/:user_id", CompileController.clearCache +app.delete "/project/:project_id/user/:user_id", CompileController.clearCache app.get "/project/:project_id/user/:user_id/sync/code", CompileController.syncFromCode app.get "/project/:project_id/user/:user_id/sync/pdf", CompileController.syncFromPdf diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 7e1f0789..d4dddd4d 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -45,7 +45,7 @@ module.exports = CompileController = } clearCache: (req, res, next = (error) ->) -> - ProjectPersistenceManager.clearProject req.params.project_id, (error) -> + ProjectPersistenceManager.clearProject req.params.project_id, req.params.user_id, (error) -> return next(error) if error? res.sendStatus(204) # No content diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index cd332a4f..bb93dbdb 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -9,7 +9,9 @@ Metrics = require "./Metrics" child_process = require "child_process" DraftModeManager = require "./DraftModeManager" fs = require("fs") +fse = require "fs-extra" os = require("os") +async = require "async" commandRunner = Settings.clsi?.commandRunner or "./CommandRunner" logger.info commandRunner:commandRunner, "selecting command runner for clsi" @@ -76,12 +78,12 @@ module.exports = CompileManager = OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> callback null, newOutputFiles - clearProject: (project_id, _callback = (error) ->) -> + clearProject: (project_id, user_id, _callback = (error) ->) -> callback = (error) -> _callback(error) _callback = () -> - compileDir = Path.join(Settings.path.compilesDir, project_id) + compileDir = getCompileDir(project_id, user_id) CompileManager._checkDirectory compileDir, (err, exists) -> return callback(err) if err? @@ -100,6 +102,27 @@ module.exports = CompileManager = else return callback(new Error("rm -r #{compileDir} failed: #{stderr}")) + _findAllDirs: (callback = (error, allDirs) ->) -> + root = Settings.path.compilesDir + fs.readdir root, (err, files) -> + return callback(err) if err? + allDirs = (Path.join(root, file) for file in files) + callback(null, allDirs) + + clearExpiredProjects: (max_cache_age_ms, callback = (error) ->) -> + now = Date.now() + # action for each directory + expireIfNeeded = (checkDir, cb) -> + fs.stat checkDir, (err, stats) -> + return cb() if err? # ignore errors checking directory + age = now - stats.mtime + hasExpired = (age > max_cache_age_ms) + if hasExpired then fse.remove(checkDir, cb) else cb() + # iterate over all project directories + CompileManager._findAllDirs (error, allDirs) -> + return callback() if error? + async.eachSeries allDirs, expireIfNeeded, callback + _checkDirectory: (compileDir, callback = (error, exists) ->) -> fs.lstat compileDir, (err, stats) -> if err?.code is 'ENOENT' diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index f70f43ca..200d977f 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -27,21 +27,30 @@ module.exports = ProjectPersistenceManager = jobs = for project_id in (project_ids or []) do (project_id) -> (callback) -> - ProjectPersistenceManager.clearProject project_id, (err) -> + ProjectPersistenceManager.clearProjectFromCache project_id, (err) -> if err? logger.error err: err, project_id: project_id, "error clearing project" callback() - async.series jobs, callback + async.series jobs, (error) -> + return callback(error) if error? + CompileManager.clearExpiredProjects ProjectPersistenceManager.EXPIRY_TIMEOUT, (error) -> + callback() # ignore any errors from deleting directories - clearProject: (project_id, callback = (error) ->) -> - logger.log project_id: project_id, "clearing project" - CompileManager.clearProject project_id, (error) -> + clearProject: (project_id, user_id, callback = (error) ->) -> + logger.log project_id: project_id, "clearing project for user" + CompileManager.clearProject project_id, user_id, (error) -> return callback(error) if error? - UrlCache.clearProject project_id, (error) -> + ProjectPersistenceManager.clearProjectFromCache project_id, (error) -> return callback(error) if error? - ProjectPersistenceManager._clearProjectFromDatabase project_id, (error) -> - return callback(error) if error? - callback() + callback() + + clearProjectFromCache: (project_id, callback = (error) ->) -> + logger.log project_id: project_id, "clearing project from cache" + UrlCache.clearProject project_id, (error) -> + return callback(error) if error? + ProjectPersistenceManager._clearProjectFromDatabase project_id, (error) -> + return callback(error) if error? + callback() _clearProjectFromDatabase: (project_id, callback = (error) ->) -> db.Project.destroy(where: {project_id: project_id}) @@ -54,5 +63,4 @@ module.exports = ProjectPersistenceManager = callback null, projects.map((project) -> project.project_id) ).error callback - logger.log {EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout" diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 0ba62b95..611ed11b 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -105,12 +105,12 @@ describe "CompileManager", -> @proc.stdout = new EventEmitter() @proc.stderr = new EventEmitter() @child_process.spawn = sinon.stub().returns(@proc) - @CompileManager.clearProject @project_id, @callback + @CompileManager.clearProject @project_id, @user_id, @callback @proc.emit "close", 0 it "should remove the project directory", -> @child_process.spawn - .calledWith("rm", ["-r", "#{@Settings.path.compilesDir}/#{@project_id}"]) + .calledWith("rm", ["-r", "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}"]) .should.equal true it "should call the callback", -> @@ -124,13 +124,13 @@ describe "CompileManager", -> @proc.stdout = new EventEmitter() @proc.stderr = new EventEmitter() @child_process.spawn = sinon.stub().returns(@proc) - @CompileManager.clearProject @project_id, @callback + @CompileManager.clearProject @project_id, @user_id, @callback @proc.stderr.emit "data", @error = "oops" @proc.emit "close", 1 it "should remove the project directory", -> @child_process.spawn - .calledWith("rm", ["-r", "#{@Settings.path.compilesDir}/#{@project_id}"]) + .calledWith("rm", ["-r", "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}"]) .should.equal true it "should call the callback with an error from the stderr", -> @@ -138,7 +138,7 @@ describe "CompileManager", -> .calledWith(new Error()) .should.equal true - @callback.args[0][0].message.should.equal "rm -r #{@Settings.path.compilesDir}/#{@project_id} failed: #{@error}" + @callback.args[0][0].message.should.equal "rm -r #{@Settings.path.compilesDir}/#{@project_id}-#{@user_id} failed: #{@error}" describe "syncing", -> beforeEach -> diff --git a/test/unit/coffee/ProjectPersistenceManagerTests.coffee b/test/unit/coffee/ProjectPersistenceManagerTests.coffee index f8c78ef7..69bfd4fa 100644 --- a/test/unit/coffee/ProjectPersistenceManagerTests.coffee +++ b/test/unit/coffee/ProjectPersistenceManagerTests.coffee @@ -13,6 +13,7 @@ describe "ProjectPersistenceManager", -> "./db": @db = {} @callback = sinon.stub() @project_id = "project-id-123" + @user_id = "1234" describe "clearExpiredProjects", -> beforeEach -> @@ -21,12 +22,13 @@ describe "ProjectPersistenceManager", -> "project-id-2" ] @ProjectPersistenceManager._findExpiredProjectIds = sinon.stub().callsArgWith(0, null, @project_ids) - @ProjectPersistenceManager.clearProject = sinon.stub().callsArg(1) + @ProjectPersistenceManager.clearProjectFromCache = sinon.stub().callsArg(1) + @CompileManager.clearExpiredProjects = sinon.stub().callsArg(1) @ProjectPersistenceManager.clearExpiredProjects @callback it "should clear each expired project", -> for project_id in @project_ids - @ProjectPersistenceManager.clearProject + @ProjectPersistenceManager.clearProjectFromCache .calledWith(project_id) .should.equal true @@ -37,8 +39,8 @@ describe "ProjectPersistenceManager", -> beforeEach -> @ProjectPersistenceManager._clearProjectFromDatabase = sinon.stub().callsArg(1) @UrlCache.clearProject = sinon.stub().callsArg(1) - @CompileManager.clearProject = sinon.stub().callsArg(1) - @ProjectPersistenceManager.clearProject @project_id, @callback + @CompileManager.clearProject = sinon.stub().callsArg(2) + @ProjectPersistenceManager.clearProject @project_id, @user_id, @callback it "should clear the project from the database", -> @ProjectPersistenceManager._clearProjectFromDatabase @@ -52,7 +54,7 @@ describe "ProjectPersistenceManager", -> it "should clear the project compile folder", -> @CompileManager.clearProject - .calledWith(@project_id) + .calledWith(@project_id, @user_id) .should.equal true it "should call the callback", -> From 6e017ecaf10edd02f8d1d3d5fef5c40d80c8ba6a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 31 May 2016 16:12:55 +0100 Subject: [PATCH 131/709] log user_id when clearing project --- app/coffee/ProjectPersistenceManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 200d977f..403043fa 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -37,7 +37,7 @@ module.exports = ProjectPersistenceManager = callback() # ignore any errors from deleting directories clearProject: (project_id, user_id, callback = (error) ->) -> - logger.log project_id: project_id, "clearing project for user" + logger.log project_id: project_id, user_id:user_id, "clearing project for user" CompileManager.clearProject project_id, user_id, (error) -> return callback(error) if error? ProjectPersistenceManager.clearProjectFromCache project_id, (error) -> From da02661d537e08a7f2bf5a4dba7a70d0de33bfa6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 7 Jun 2016 14:38:17 +0100 Subject: [PATCH 132/709] add random string to smoke tests to avoid collision --- test/smoke/coffee/SmokeTests.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/smoke/coffee/SmokeTests.coffee b/test/smoke/coffee/SmokeTests.coffee index 48c50d66..ca3082c9 100644 --- a/test/smoke/coffee/SmokeTests.coffee +++ b/test/smoke/coffee/SmokeTests.coffee @@ -6,10 +6,14 @@ Settings = require "settings-sharelatex" buildUrl = (path) -> "http://#{Settings.internal.clsi.host}:#{Settings.internal.clsi.port}/#{path}" +random = require("crypto").randomBytes(4).toString("hex") + +url = buildUrl("project/smoketest-#{random}/compile") + describe "Running a compile", -> before (done) -> request.post { - url: buildUrl("project/smoketest/compile") + url: url json: compile: resources: [ From b97627d6d81c835ac2297ae9eb8a27d701840449 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 7 Jun 2016 14:47:51 +0100 Subject: [PATCH 133/709] use process id so link process to smoke test --- test/smoke/coffee/SmokeTests.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/smoke/coffee/SmokeTests.coffee b/test/smoke/coffee/SmokeTests.coffee index ca3082c9..372ca69d 100644 --- a/test/smoke/coffee/SmokeTests.coffee +++ b/test/smoke/coffee/SmokeTests.coffee @@ -6,9 +6,7 @@ Settings = require "settings-sharelatex" buildUrl = (path) -> "http://#{Settings.internal.clsi.host}:#{Settings.internal.clsi.port}/#{path}" -random = require("crypto").randomBytes(4).toString("hex") - -url = buildUrl("project/smoketest-#{random}/compile") +url = buildUrl("project/smoketest-#{process.pid}/compile") describe "Running a compile", -> before (done) -> From 0b8435e3581b3b576c7fbcf3add835e7aa8ac7c9 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 15 Jun 2016 16:12:19 +0100 Subject: [PATCH 134/709] add route to serve files from top level of per user containers --- app.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app.coffee b/app.coffee index 6a8d73fe..fb3224e5 100644 --- a/app.coffee +++ b/app.coffee @@ -96,6 +96,11 @@ app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) -> req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}") staticServer(req, res, next) +app.get "/project/:project_id/user/:user_id/output/*", (req, res, next) -> + # for specific user get the path to the top level file + req.url = "/#{req.params.project_id}-#{req.params.user_id}/#{req.params[0]}" + staticServer(req, res, next) + app.get "/project/:project_id/output/*", (req, res, next) -> if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX) # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) From e4ffc94de80b5a8d33a11ee2b096e8a7157f9168 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Fri, 17 Jun 2016 14:38:08 +0100 Subject: [PATCH 135/709] 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"]`. --- app/coffee/LatexRunner.coffee | 4 +++- config/settings.defaults.coffee | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 65a30464..748c277f 100644 --- a/app/coffee/LatexRunner.coffee +++ b/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/config/settings.defaults.coffee b/config/settings.defaults.coffee index ae8e1323..f77df86b 100644 --- a/config/settings.defaults.coffee +++ b/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 8bb12f4d996cdbd1c2f096ac82e464af231f7bfc Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Mon, 20 Jun 2016 09:31:30 +0100 Subject: [PATCH 136/709] Upgrade to node 4.2 --- .nvmrc | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..bf77d549 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +4.2 diff --git a/package.json b/package.json index eaa9be8c..748256b9 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "sequelize": "^2.1.3", "wrench": "~1.5.4", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~2.2.0", + "sqlite3": "~3.1.4", "express": "^4.2.0", "body-parser": "^1.2.0", "fs-extra": "^0.16.3", From c486d6c2159bd0abc0fbcaca946684d884464772 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 8 Jun 2016 16:58:08 +0100 Subject: [PATCH 137/709] only keep a single cached output directory in per-user containers --- app/coffee/OutputCacheManager.coffee | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 7f11bc89..25c24b88 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/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 d29416fc77eab5f8a8b023e4ac41678be29eb617 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 29 Jun 2016 16:31:08 +0100 Subject: [PATCH 138/709] keep one extra build until per-page pdf serving is enabled --- app/coffee/OutputCacheManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 25c24b88..76692b3b 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/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 d6808c11cc494bde1cbd4b1813f17dfd10f25ed9 Mon Sep 17 00:00:00 2001 From: WaeCo <aaqqws@googlemail.com> Date: Wed, 13 Jul 2016 13:26:32 -0700 Subject: [PATCH 139/709] 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. --- config/settings.defaults.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index f77df86b..f1f7492a 100644 --- a/config/settings.defaults.coffee +++ b/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 69666bef60ed3a1148ce3eb928ceeda3e76c2da7 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 14 Jul 2016 14:47:36 +0100 Subject: [PATCH 140/709] add support for stopping compile --- app.coffee | 2 ++ app/coffee/CommandRunner.coffee | 24 +++++++++++++++++++++--- app/coffee/CompileController.coffee | 12 ++++++++++-- app/coffee/CompileManager.coffee | 11 +++++++++++ app/coffee/LatexRunner.coffee | 15 ++++++++++++++- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/app.coffee b/app.coffee index fb3224e5..d13798c4 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 5ea6765c..1d0b9e41 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 d4dddd4d..610baac2 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 bb93dbdb..87e87345 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 748c277f..8619e9ba 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"] ) From fdf274fb829736f042c6d4f95730eabaf91e6f99 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 18 Jul 2016 11:05:45 +0100 Subject: [PATCH 141/709] remove dead code --- app/coffee/CompileController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 610baac2..38d53f93 100644 --- a/app/coffee/CompileController.coffee +++ b/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 652443969968cc053ac99541bc2de6a1e8d5e154 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 26 Jul 2016 12:30:29 +0100 Subject: [PATCH 142/709] add support for passing additional environment parameters to command runner includes an example of passing environment variables to chktex --- app/coffee/CommandRunner.coffee | 9 +++++++-- app/coffee/CompileManager.coffee | 10 +++++++++- app/coffee/LatexRunner.coffee | 6 +++--- test/unit/coffee/CompileManagerTests.coffee | 2 ++ test/unit/coffee/LatexRunnerTests.coffee | 6 ++++-- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 1d0b9e41..298a4950 100644 --- a/app/coffee/CommandRunner.coffee +++ b/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/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 87e87345..81c2c039 100644 --- a/app/coffee/CompileManager.coffee +++ b/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/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 8619e9ba..e743cf01 100644 --- a/app/coffee/LatexRunner.coffee +++ b/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/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 611ed11b..62132f89 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/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/test/unit/coffee/LatexRunnerTests.coffee b/test/unit/coffee/LatexRunnerTests.coffee index ace3d18a..c26fa642 100644 --- a/test/unit/coffee/LatexRunnerTests.coffee +++ b/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 14837a57ecacbcdc437d1fe00c3e1fa03113a518 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 26 Jul 2016 16:22:38 +0100 Subject: [PATCH 143/709] run chktex when request has check:true --- app/coffee/CommandRunner.coffee | 4 ++++ app/coffee/CompileController.coffee | 2 ++ app/coffee/CompileManager.coffee | 8 ++++---- app/coffee/RequestParser.coffee | 3 +++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 298a4950..11bbff82 100644 --- a/app/coffee/CommandRunner.coffee +++ b/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/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 38d53f93..3c9af677 100644 --- a/app/coffee/CompileController.coffee +++ b/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/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 81c2c039..3b19ceca 100644 --- a/app/coffee/CompileManager.coffee +++ b/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/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index bd081fd0..5979c75c 100644 --- a/app/coffee/RequestParser.coffee +++ b/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 664e908378389f1e12f30e274084a9aa99435c81 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 27 Jul 2016 16:54:27 +0100 Subject: [PATCH 144/709] provide validation mode where compilation always exits after chktex --- app/coffee/CompileController.coffee | 4 ++-- app/coffee/CompileManager.coffee | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 3c9af677..56237bf2 100644 --- a/app/coffee/CompileController.coffee +++ b/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/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 3b19ceca..4c89a0ba 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 8da29e602499d24de339088963b23de28eb0e86e Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 29 Jul 2016 14:54:24 +0100 Subject: [PATCH 145/709] provide setting to override child_process.execFile for synctex --- app/coffee/CompileManager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 87e87345..16c36471 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 c6744caeeba306c1c2525505666d360336fddc83 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 4 Aug 2016 16:07:36 +0100 Subject: [PATCH 146/709] change logging message to be different from LatexRunner --- app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index bf71477c..5d14bdf0 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 e66b1ecdeaaf4011c49b0e74789be476aa6db560 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 4 Aug 2016 16:08:14 +0100 Subject: [PATCH 147/709] use a command wrapper for synctex instead of an alternative child_process object --- app/coffee/CompileManager.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 5d14bdf0..34a3a57b 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 ade3da7e0d6662e13f33259e6e3027c541dceb1c Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 11 Aug 2016 09:29:03 +0100 Subject: [PATCH 148/709] add missing argument parameter to wordcount call --- app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 34a3a57b..39cff29d 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 928ffc96e6d5ce29ce36235e6ac7bd3f52697269 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 11 Aug 2016 09:32:47 +0100 Subject: [PATCH 149/709] read wordcount output asynchronously --- app/coffee/CompileManager.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 39cff29d..e3a218be 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 2200ac2cf2808d8d8bde92a082df11141f60d83e Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 11 Aug 2016 09:40:08 +0100 Subject: [PATCH 150/709] capture texcount error output --- app/coffee/CompileManager.coffee | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index e3a218be..5192b7fa 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 cd7ed6ce6661f161e02a0dfc1f3cc3f02141953b Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 11 Aug 2016 10:31:37 +0100 Subject: [PATCH 151/709] update tests --- test/unit/coffee/CompileManagerTests.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 62132f89..d2b6a100 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/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 748caeee7d671d69ffe5ad1ad50aa77c5560524a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 22 Aug 2016 15:11:39 +0100 Subject: [PATCH 152/709] remove chktex error too many false positives from 'unable to execute latex command' --- app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 5192b7fa..356346d3 100644 --- a/app/coffee/CompileManager.coffee +++ b/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 3a73971b4263eeb3b74d23415029aa1da158f4ee Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 24 Aug 2016 15:45:26 +0100 Subject: [PATCH 153/709] fix commandRunner error to match dockerRunner --- app/coffee/CommandRunner.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 11bbff82..f47af001 100644 --- a/app/coffee/CommandRunner.coffee +++ b/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 095e16e95353aebcca514d052adc08bfc4f56c2a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 24 Aug 2016 15:46:47 +0100 Subject: [PATCH 154/709] handle failed compile due to validation error --- app/coffee/CompileManager.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 356346d3..5d2bebb3 100644 --- a/app/coffee/CompileManager.coffee +++ b/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? From 4128dc6fddfe26f576bd0f765ab80bdb0bbea874 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Thu, 1 Sep 2016 09:53:12 +0100 Subject: [PATCH 155/709] Revert "Upgrade to node 4.2" This reverts commit 8bb12f4d996cdbd1c2f096ac82e464af231f7bfc. --- .nvmrc | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index bf77d549..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -4.2 diff --git a/package.json b/package.json index 748256b9..eaa9be8c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "sequelize": "^2.1.3", "wrench": "~1.5.4", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~3.1.4", + "sqlite3": "~2.2.0", "express": "^4.2.0", "body-parser": "^1.2.0", "fs-extra": "^0.16.3", From 98fb2cab99d11df3dae3e27e8b75ff1f41b10890 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Thu, 1 Sep 2016 11:22:11 +0100 Subject: [PATCH 156/709] Revert "Revert "Upgrade to node 4.2"" This reverts commit 4128dc6fddfe26f576bd0f765ab80bdb0bbea874. --- .nvmrc | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..bf77d549 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +4.2 diff --git a/package.json b/package.json index eaa9be8c..748256b9 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "sequelize": "^2.1.3", "wrench": "~1.5.4", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~2.2.0", + "sqlite3": "~3.1.4", "express": "^4.2.0", "body-parser": "^1.2.0", "fs-extra": "^0.16.3", From 47105190becd0cc7ad17255715162c3bdf2c0162 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Thu, 1 Sep 2016 12:47:13 +0100 Subject: [PATCH 157/709] Revert "Revert "Revert "Upgrade to node 4.2""" This reverts commit 98fb2cab99d11df3dae3e27e8b75ff1f41b10890. --- .nvmrc | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index bf77d549..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -4.2 diff --git a/package.json b/package.json index 748256b9..eaa9be8c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "sequelize": "^2.1.3", "wrench": "~1.5.4", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~3.1.4", + "sqlite3": "~2.2.0", "express": "^4.2.0", "body-parser": "^1.2.0", "fs-extra": "^0.16.3", From ee170b4e674d4284c55c8d85cb2d79e612c0055d Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 12 Sep 2016 16:28:52 +0100 Subject: [PATCH 158/709] only run chktex on .tex files, not .Rtex files the .tex files produced from knitr have macros which confuse chktex --- app/coffee/CompileManager.coffee | 4 ++- test/unit/coffee/CompileManagerTests.coffee | 35 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 5d2bebb3..a47f0f8e 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -44,7 +44,9 @@ module.exports = CompileManager = # set up environment variables for chktex env = {} - if request.check? + # only run chktex on LaTeX files (not knitr .Rtex files or any others) + isLaTeXFile = request.rootResourcePath?.match(/\.tex$/i) + if request.check? and isLaTeXFile env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16' if request.check is 'error' env['CHKTEX_EXIT_ON_ERROR'] = 1 diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index d2b6a100..5b07124c 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -98,6 +98,41 @@ describe "CompileManager", -> .calledWith(@compileDir + "/" + @rootResourcePath) .should.equal true + describe "with a check option", -> + beforeEach -> + @request.check = "error" + @CompileManager.doCompile @request, @callback + + it "should run chktex", -> + @LatexRunner.runLatex + .calledWith("#{@project_id}-#{@user_id}", { + directory: @compileDir + mainFile: @rootResourcePath + compiler: @compiler + timeout: @timeout + image: @image + environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1} + }) + .should.equal true + + describe "with a knitr file and check options", -> + beforeEach -> + @request.rootResourcePath = "main.Rtex" + @request.check = "error" + @CompileManager.doCompile @request, @callback + + it "should not run chktex", -> + @LatexRunner.runLatex + .calledWith("#{@project_id}-#{@user_id}", { + directory: @compileDir + mainFile: "main.Rtex" + compiler: @compiler + timeout: @timeout + image: @image + environment: @env + }) + .should.equal true + describe "clearProject", -> describe "succesfully", -> beforeEach -> From 79b3d2172b929d0bff1b8f6cbd937f308084f289 Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Wed, 21 Sep 2016 15:09:01 +0100 Subject: [PATCH 159/709] Sanitize resource path along with rootResourcePath --- app/coffee/RequestParser.coffee | 8 +++++++- test/unit/coffee/RequestParserTests.coffee | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 5979c75c..90bc739f 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -42,7 +42,13 @@ module.exports = RequestParser = compile.rootResourcePath default: "main.tex" type: "string" - response.rootResourcePath = RequestParser._sanitizePath(rootResourcePath) + originalRootResourcePath = rootResourcePath + sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath) + response.rootResourcePath = sanitizedRootResourcePath + + for resource in response.resources + if resource.path == originalRootResourcePath + resource.path = sanitizedRootResourcePath catch error return callback error diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index 8545ff22..4cf61198 100644 --- a/test/unit/coffee/RequestParserTests.coffee +++ b/test/unit/coffee/RequestParserTests.coffee @@ -206,11 +206,21 @@ describe "RequestParser", -> describe "with a root resource path that needs escaping", -> beforeEach -> - @validRequest.compile.rootResourcePath = "`rm -rf foo`.tex" + @badPath = "`rm -rf foo`.tex" + @goodPath = "rm -rf foo.tex" + @validRequest.compile.rootResourcePath = @badPath + @validRequest.compile.resources.push { + path: @badPath + date: "12:00 01/02/03" + content: "Hello world" + } @RequestParser.parse @validRequest, @callback @data = @callback.args[0][1] it "should return the escaped resource", -> - @data.rootResourcePath.should.equal "rm -rf foo.tex" + @data.rootResourcePath.should.equal @goodPath + + it "should also escape the resource path", -> + @data.resources[0].path.should.equal @goodPath From f7b4883397fe4f9cca07d6553fc8fbe9970c082c Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Thu, 22 Sep 2016 14:14:29 +0100 Subject: [PATCH 160/709] Don't delete knitr cache files --- app/coffee/ResourceWriter.coffee | 2 +- test/unit/coffee/ResourceWriterTests.coffee | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index e3639006..16000cbb 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -46,7 +46,7 @@ module.exports = ResourceWriter = do (file) -> path = file.path should_delete = true - if path.match(/^output\./) or path.match(/\.aux$/) + if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" should_delete = true diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 5480bd6b..c1e72a87 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -55,6 +55,8 @@ describe "ResourceWriter", -> }, { path: "extra.aux" type: "aux" + }, { + path: "cache/_chunk1" }] @resources = "mock-resources" @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @@ -80,6 +82,11 @@ describe "ResourceWriter", -> @ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(@basePath, "extra.aux")) .should.equal false + + it "should not delete the knitr cache file", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "cache/_chunk1")) + .should.equal false it "should call the callback", -> @callback.called.should.equal true From 0900340282985b0817cdc07a676360e9b29e6437 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Fri, 23 Sep 2016 15:32:37 +0100 Subject: [PATCH 161/709] Add CHKTEX_ULIMIT_OPTIONS --- app/coffee/CompileManager.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index a47f0f8e..fa8c9faf 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -48,6 +48,7 @@ module.exports = CompileManager = isLaTeXFile = request.rootResourcePath?.match(/\.tex$/i) if request.check? and isLaTeXFile env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16' + env['CHKTEX_ULIMIT_OPTIONS'] = '-t 5 -v 16000' if request.check is 'error' env['CHKTEX_EXIT_ON_ERROR'] = 1 if request.check is 'validate' From 4827aec30bd1089c348a6c072fa5b89ca3739468 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Fri, 23 Sep 2016 15:34:29 +0100 Subject: [PATCH 162/709] Add test for new ulimit options --- test/unit/coffee/CompileManagerTests.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 5b07124c..3ddf73b1 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -111,7 +111,7 @@ describe "CompileManager", -> compiler: @compiler timeout: @timeout image: @image - environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1} + environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1, 'CHKTEX_ULIMIT_OPTIONS': '-t 5 -v 16000'} }) .should.equal true From 61089eca40d4656c35a8310bcef6b1443784a326 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Wed, 28 Sep 2016 11:02:58 +0100 Subject: [PATCH 163/709] Increase memory limit to 64mb --- app/coffee/CompileManager.coffee | 2 +- test/unit/coffee/CompileManagerTests.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index fa8c9faf..0a1ea9d1 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -48,7 +48,7 @@ module.exports = CompileManager = isLaTeXFile = request.rootResourcePath?.match(/\.tex$/i) if request.check? and isLaTeXFile env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16' - env['CHKTEX_ULIMIT_OPTIONS'] = '-t 5 -v 16000' + env['CHKTEX_ULIMIT_OPTIONS'] = '-t 5 -v 64000' if request.check is 'error' env['CHKTEX_EXIT_ON_ERROR'] = 1 if request.check is 'validate' diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 3ddf73b1..e3f0705d 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -111,7 +111,7 @@ describe "CompileManager", -> compiler: @compiler timeout: @timeout image: @image - environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1, 'CHKTEX_ULIMIT_OPTIONS': '-t 5 -v 16000'} + environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1, 'CHKTEX_ULIMIT_OPTIONS': '-t 5 -v 64000'} }) .should.equal true From 9e53c0b99e7973e67e5dbdc0e85e12fe2ff53939 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 14 Oct 2016 10:23:13 +0100 Subject: [PATCH 164/709] fix exception in error log --- app/coffee/OutputCacheManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 76692b3b..91e085ae 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -85,7 +85,7 @@ module.exports = OutputCacheManager = # 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" + logger.error err: err, dir: cacheDir, "error removing cache dir after failure" else # pass back the list of new files in the cache callback(err, results) From 0530e212469e7ef4d64f93fecac0ed42d50011a1 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 24 Jan 2017 11:07:54 +0000 Subject: [PATCH 165/709] fix acceptance tests --- test/acceptance/coffee/TimeoutTests.coffee | 3 ++- test/acceptance/coffee/WordcountTests.coffee | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index 66ac7a42..5e0058d3 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -6,13 +6,14 @@ describe "Timed out compile", -> before (done) -> @request = options: - timeout: 0.01 #seconds + timeout: 1 #seconds resources: [ path: "main.tex" content: ''' \\documentclass{article} \\begin{document} Hello world + \\input{|"sleep 10"} \\end{document} ''' ] diff --git a/test/acceptance/coffee/WordcountTests.coffee b/test/acceptance/coffee/WordcountTests.coffee index 1789cfcd..d84ecba3 100644 --- a/test/acceptance/coffee/WordcountTests.coffee +++ b/test/acceptance/coffee/WordcountTests.coffee @@ -29,6 +29,8 @@ describe "Syncing", -> elements: 0 mathInline: 6 mathDisplay: 0 + errors: 0 + messages: "" } ) done() From dab92967c8b643501cde6c5a1f2cf66c1a3a3f67 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 24 Jan 2017 12:18:30 +0000 Subject: [PATCH 166/709] added docker script for acceptance tests --- test/acceptance/scripts/full-test.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 test/acceptance/scripts/full-test.sh diff --git a/test/acceptance/scripts/full-test.sh b/test/acceptance/scripts/full-test.sh new file mode 100755 index 00000000..f11ca3c6 --- /dev/null +++ b/test/acceptance/scripts/full-test.sh @@ -0,0 +1,23 @@ +#! /usr/bin/env bash + +npm rebuild + +echo ">> Starting server..." + +grunt --no-color & + +echo ">> Server started" + +sleep 5 + +echo ">> Running acceptance tests..." +grunt --no-color test:acceptance +_test_exit_code=$? + +echo ">> Killing server" + +kill %1 + +echo ">> Done" + +exit $_test_exit_code From 420db18a03f2a60f0b25600d0142dd37c68571ef Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 24 Jan 2017 16:06:32 +0000 Subject: [PATCH 167/709] upgrade to latest sqlite3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eaa9be8c..65367df1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "sequelize": "^2.1.3", "wrench": "~1.5.4", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~2.2.0", + "sqlite3": "~3.1.8", "express": "^4.2.0", "body-parser": "^1.2.0", "fs-extra": "^0.16.3", From 03e837c1f46e4c6258967992a195647e410be7fe Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 25 Jan 2017 14:08:39 +0000 Subject: [PATCH 168/709] run tests outside container, add settings file --- test/acceptance/scripts/full-test.sh | 2 +- test/acceptance/scripts/settings.test.coffee | 46 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 test/acceptance/scripts/settings.test.coffee diff --git a/test/acceptance/scripts/full-test.sh b/test/acceptance/scripts/full-test.sh index f11ca3c6..fa277e1e 100755 --- a/test/acceptance/scripts/full-test.sh +++ b/test/acceptance/scripts/full-test.sh @@ -1,6 +1,6 @@ #! /usr/bin/env bash -npm rebuild +export SHARELATEX_CONFIG=`pwd`/test/acceptance/scripts/settings.test.coffee echo ">> Starting server..." diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.coffee new file mode 100644 index 00000000..1c94c40e --- /dev/null +++ b/test/acceptance/scripts/settings.test.coffee @@ -0,0 +1,46 @@ +Path = require "path" + +module.exports = + # Options are passed to Sequelize. + # See http://sequelizejs.com/documentation#usage-options for details + mysql: + clsi: + database: "clsi" + username: "clsi" + password: null + dialect: "sqlite" + storage: Path.resolve("db.sqlite") + + path: + compilesDir: Path.resolve(__dirname + "/../../../compiles") + clsiCacheDir: Path.resolve(__dirname + "/../../../cache") + #synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) + synctexBaseDir: () -> "/compile" + + clsi: + #strace: true + #archive_logs: true + commandRunner: "docker-runner-sharelatex" + docker: + image: "quay.io/sharelatex/texlive-full:2015.1" + env: + PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2015/bin/x86_64-linux/" + HOME: "/tmp" + modem: + socketPath: false + user: "tex" + latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux + + internal: + clsi: + port: 3013 + load_port: 3044 + host: "localhost" + + apis: + clsi: + url: "http://localhost:3013" + + smokeTest: false + project_cache_length_ms: 1000 * 60 * 60 * 24 + parallelFileDownloads:1 From b9d6db6caf62c57caecd8af6e6feb5d02b8aea08 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 25 Jan 2017 14:09:44 +0000 Subject: [PATCH 169/709] use local docker image for clsi test --- test/acceptance/scripts/settings.test.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.coffee index 1c94c40e..257019f4 100644 --- a/test/acceptance/scripts/settings.test.coffee +++ b/test/acceptance/scripts/settings.test.coffee @@ -22,9 +22,9 @@ module.exports = #archive_logs: true commandRunner: "docker-runner-sharelatex" docker: - image: "quay.io/sharelatex/texlive-full:2015.1" + image: "texlive-full:2016.1" env: - PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2015/bin/x86_64-linux/" + PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2016/bin/x86_64-linux/" HOME: "/tmp" modem: socketPath: false From 654a43655f61a8b3671ef35356ffd42944aafc47 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 25 Jan 2017 14:12:19 +0000 Subject: [PATCH 170/709] update image for docker tests --- test/acceptance/scripts/settings.test.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.coffee index 257019f4..b886279c 100644 --- a/test/acceptance/scripts/settings.test.coffee +++ b/test/acceptance/scripts/settings.test.coffee @@ -22,7 +22,7 @@ module.exports = #archive_logs: true commandRunner: "docker-runner-sharelatex" docker: - image: "texlive-full:2016.1" + image: "texlive-full:2016.1-opt" env: PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2016/bin/x86_64-linux/" HOME: "/tmp" From 146138f65c1f9386dc82d2a154c374365c361f6c Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 26 Jan 2017 12:06:38 +0000 Subject: [PATCH 171/709] try running user as jenkins --- test/acceptance/scripts/settings.test.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.coffee index b886279c..a1f51ddb 100644 --- a/test/acceptance/scripts/settings.test.coffee +++ b/test/acceptance/scripts/settings.test.coffee @@ -28,7 +28,7 @@ module.exports = HOME: "/tmp" modem: socketPath: false - user: "tex" + user: "111" latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux internal: From f00be9018d6779f76c576520afd35f5eee7d8e21 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 26 Jan 2017 12:20:41 +0000 Subject: [PATCH 172/709] log acceptance test server output to file --- test/acceptance/scripts/full-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/scripts/full-test.sh b/test/acceptance/scripts/full-test.sh index fa277e1e..4b164532 100755 --- a/test/acceptance/scripts/full-test.sh +++ b/test/acceptance/scripts/full-test.sh @@ -4,7 +4,7 @@ export SHARELATEX_CONFIG=`pwd`/test/acceptance/scripts/settings.test.coffee echo ">> Starting server..." -grunt --no-color & +grunt --no-color >server.log 2>&1 & echo ">> Server started" From b76a81e98b5345778eccb0d60f3a19a7536e30eb Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 27 Jan 2017 12:21:57 +0000 Subject: [PATCH 173/709] specify papersize explicitly in latex test --- test/acceptance/fixtures/examples/latex_compiler/main.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/fixtures/examples/latex_compiler/main.tex b/test/acceptance/fixtures/examples/latex_compiler/main.tex index 97b68886..76fd8e5f 100644 --- a/test/acceptance/fixtures/examples/latex_compiler/main.tex +++ b/test/acceptance/fixtures/examples/latex_compiler/main.tex @@ -1,4 +1,4 @@ -\documentclass{article} +\documentclass[a4paper]{article} \usepackage{graphicx} From ea484da9f41af2b69b7aa54dfa1bfaf976baf28f Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 27 Jan 2017 12:32:14 +0000 Subject: [PATCH 174/709] update latex_compiler test pdf --- .../examples/latex_compiler/output.pdf | Bin 23194 -> 30826 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/latex_compiler/output.pdf b/test/acceptance/fixtures/examples/latex_compiler/output.pdf index e5d3d1f242362990415d146536fda6f27ebce473..af1f6aafd6b30803823cf29346090c7034ba22a4 100644 GIT binary patch delta 29965 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\d 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 O%Y#5gC8Z>d@V@}h_+p{} literal 23194 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 Xw81FT8>v9UaTXhe92~@6Q=$I=GIJf; From d238f73e29a1cb6c1ebe91b3676bb6b4e0611903 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 30 Jan 2017 15:37:26 +0000 Subject: [PATCH 175/709] try output.pdf generated with texlive 2016 --- .../examples/latex_compiler/output.pdf | Bin 30826 -> 24239 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/latex_compiler/output.pdf b/test/acceptance/fixtures/examples/latex_compiler/output.pdf index af1f6aafd6b30803823cf29346090c7034ba22a4..b87319f7d40b8c57b3e3153253f0cc6533375cf4 100644 GIT binary patch delta 23242 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 Wr?0z>Ek3`1n1CQY7_6YFi2q+5hO`U- delta 29837 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 H)Ae<2HmABHFk$~4bA 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 From bc73f719b297c840de4322dff2d16a1ae42e37f5 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 31 Jan 2017 09:53:36 +0000 Subject: [PATCH 176/709] update asymptote pdf to a4 size for texlive 2016 --- .../fixtures/examples/asymptote/output.pdf | Bin 121369 -> 122452 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/asymptote/output.pdf b/test/acceptance/fixtures/examples/asymptote/output.pdf index 0a7fb42c99125edefec74cb4b2c5217690998d39..942b9154967cd1f79d9ad467cc431d0244d29b45 100644 GIT binary patch delta 115713 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%0D)pmou}eCCEI$3V7co zxhqG{XmfeMzEjk0e;uODi$c8cia8{9im9tXioJ(^&3!(JnBT2zz_b<!pNt8S&w4#d z<hvOs&CsXMOf=qjGg#kLEWRiZ_N`dOY0N00m%f0l=KGIAxKb1YXP1$)*yRYfaAs3V zXTE>9X-+>TuiS}^cujOx<CzU&wbV=U9@r&g<D|btk+2q4Ev^N<`=mKvj>Tjr?n&Sl z4l{wKExCTU?Cxot6151=*<$>CK8SQAa~2h2?1G#mZuQa6zO^|^s;k*3u|XVV5#*0L z1=p0dZ@8D5MdamEy4z~DB%_(HtPHXy>u-N`JK2AK4_z{4IWB2?z_+Wx3|L|mHfF(7 zWXo%~PbOnVa&smIKhuT!6kJfLwBO~zsqCVTWiM`E+sZT7H4afNeJhKo<E;f>@J}fu zyVzVqr8)LGyz)P?+vs>42#FZ(FUcKJMpm2hiFOm`P5<x;BKG%3yj-d||1suhcO-wa zRDL3QdSV8IP7xZ&lirWcPb(9Pk1P=lPx{v2m$4ov+ls4?Fs}WS7HlcR6nEvO=ry+) z&HRIncHZ%zAiKw)18zW~X423sn$bq57898ELr^aIz;®u_v@$ew0ko+ET8F!!}9 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 F{y$AYS^EG0 literal 121369 zcmagFV~j39w<X-RPusR_+qP}HPun`}?x$_rwr$(CJ@38Wo#abqlG&;3R93CMs@AVc zYNyB)MaAiu=-FV%=9Y%nV7Lew2_1~B33+*87*xF+%?KGZ6^yOTOk80YRNRbR{~Hl^ zaJGYC5EgduB-CO251)~qi;MmL%X%;j|F!zxl}s=Uaz?JsR{vo!{TGYyzw>_>%4RMO zZq6oVE`&_~1uMfa$eP((xLOi&68`V_kBpVAtC=$)gSf4ctC^^oiG!&b3_m}Ni>tGl zksXZZ1}M;f0vN{3-t>Q<|IGe#a2Tfl+U0*c_#a&+Ca(Vx{(tMz$j~Z}DabRlOph_p z&&w##-Fvt*tN^*dpe(9<OS`aDFFr)GP6S*q+rd|{*a=oVJ1ajf$S_y2G&i$VvMjO+ z8qcTKdwWH;m$$TB^psPjM*?$2rpOnQk-d9CTx2XWq79Qpl9jhuMRE<7uQWFUQK-`? z|6gpd{GT?M*;tr3{@cj^+lFhKNIX5<RNiNc-RA#qbSiWzJT`4Ve!F3KSeKBi&61aq zW~!oynI%wx0PXoBDbE`RuAHa<LjVD7Y?zq^{E7qs{q`cx{homS5U~Fyd=u#YV&Uci z0p0!*GD848{DKZ0{YGni^{N1W3n+ibd7gk2gV_3+Ay645PEY0J_DinkNG)qFkIAYH z=gbWaNve!5i7x-BG?wH>+r;PQ$EDjS)|K|^3PQLu@y+{zg4U$~=hZFu0|N78fh0|U zC{_KD$k70K1(cXzx^BLHsnb;p{>$&cEJS}G(Unnt6?TKX0g?$piTQa^R}6V!g@Dke zJkCr>CCQ<{*bs!UgQJ4rpu~^>8>BGSS>1Lm9tl*Iy?*~{ZOTPkpBoZg_hO!l#evXr z_jLthW#uQ-G?t@?JUiO&u1<xvzJ$o(NfbBzIt5d=j>VZq>XI9VFt{;_tok;>#Tfo? zS8UzR>I}D`3yjo@w!6vt{*Vt39>3mR;7pTJ8)SJyIR+_%7I`wc*nbxf8SQA#q)VB) zbYVZG_c>1lRQ%{-Mes9dIuyP+3@vD4ndFc1$Syo_1oJhz+jcPCn!6<T)=zq!LHULP zc(tfede{W9aEj~9)$38V$0x#fbyJ0GeGpTy^NW^gWZ%-!ohdd&8Zp-Kdaitc5#(U3 zN9NEu_+ka~AbP$FD)qq%S&Bi~eGEy5`32VHsA9U^veeeemTSxOCFG74Tw(=91B1o9 zZAjK#HpsnoWLxeeXf>lq%G#KMig33$H;f<SwKNFFbr|()3exluQT!G-;<`D5@ms|z zSXRGIus#>cb=SC}<Fyb}^8HNHSKE3^7wTgAT|VJBSGP-ss^gAG>RQ=?hH%e@KI<Jb z;968+W~B?x&3d(yB<EK21fM?0?BMw&%e0=}<W%*_6)ITr6-$a|#yUBaa4*KO)NgEp zVi3ZOO}oto+~YT*FR!Ese#1}S=y?m)@ins`W~>vu*|51F2>*Zq$UuLLaG_YAl=5n! zGx3IZZ6np1C~VhQlJ5Y-n9Ydcq#+sW5c9H=2M?2qGjxiP`WQ$E0h@enNUMV(QJCov z9a=%NYvMPKhyi4wcN~hsd8)KQ`YViVGhOyL=6o@P3JJr=yxx}KJZ%iCue#%))r8ad zw!+Fc20>EBI1or4%#G;50<HV|zKeqfsjdJu0KI#bN~B~kNKAG>^G7OOsv+GqyJMp! zb2tXm+bD8&5FG2&(9$sfU(mqIr=~}HNz+1fCR2b!WRRcSQouQWk&@sVM!U`Gz=_-s zF~69cAs!oou&E_Ia<nfdPJ`A%N_B!9V|%~f&?%O9BGB&)d9bxX>GPp5M77BBe5YW! zA}ZCL0+DpU`({)Ae0kbPz!Q|-{PK10TuP>X87`(+2GH+C2XLiV3R?gL#!BQ0;aO~z zwcgR)43RYWwv|qFxmc$*RzXXHmVg2>V+u3@Z(|?#IX?E81?<EVy+eQ@UCnB())w}! zTl*Ry$PI;Gv;jFusM%Q_tYvW#y&P0oN{iKV5Mx}-cd`*&{gKKAsFkK^v8g=|Pj&bQ zfXo>41awV+8CN6T`Ioav-!-k}{%a|1Ir`3@2>P_B(?OF&Pu1R$%>-<l+yTCIKz*N* z+fjxhBm7G`D_XvkUz;tYd&!q!^ca5!I`wY_?F8f+t!Th{1VN~cWVd|$KAVY*b6aKR z*i)_S!)Ens-uGfXWc4#64}-rG_35$SU5m&UK8=%qJzJ-e4eOTIEp3z|yrMaIWw|q- zyO>q3+u%xP#-ltT{i_Yuhoh}SH;|?xMv~wbR69cInt>74?%slZgP}`=bhmkG<XA8r zNt+*Qik!}Q3;Z&KBr$wf3%CGJYa_RLLlGUh(;jp?ed?V9Z^0&Rz`ziNY~QnFk9LuY zQ?V2*qhC_XVuvAOLhIG4C`=dHAN84{!6c{hfPJs$If+E_zi-r{tweB;gP1V`&ywBd zT^(|%V0@uR$E>04h8}AT2IqM{lkPy^%bVKz$K%<FscW?&l=yf0!*?wXe~GL}pYj`a zlh5(bJmN>vXh_Do@f32$aC#*i&eKG!-<qJ9j*|P&GRGQv0AC@0gng^G?jz^FjuNwO z2aemw*B3cma#^9ht#ej4ZF3r;;4`1;3E6)uNQ#=;a6_8v=*fuofD8uu9V*YE96rG% zecd@jq1+LV?&8Uk>1LCI*S+Pm^4(4O7W0_&$iN2O79Z;6Ev`E<^dXU*b1AX7z&f%c zIA^=LV}a;Co13GaNTeU`NHJ$oYdrORn(r{gVg5?O1=$o$e!`ns8OWLw2GGE#JvqD- zwU@(i-8Q+QjC=f6I3{69rXE*ovSz?nDRMjNBA3OT%xQ-7eb+B*GiOTBk<Lp(>M%>c zDfWnTag$yZ2=4ch?dmwizBLeU={ygtMxMqLns;yH@oVSJV}YD52>DN1Sm{Vrq*FZU zQ=RXAi}r4SWpkrrh9|{)t9eR)Hx%rcJO?i%oJPtRjd{!G@lVL8SgTIt4W_%(dbqG% zmqf+9>UM8SbPvGsLDO&|P;_6d>QG1rXy)u&F=p&nyq#|6yD1W!=OYtfYB*FruNvbw zCl`@`8lMNeGRP5xpMWJ6y>D~kh921jr%wb^xaHnL`gYdPv@|}lPQ4#7jW$##_3)PR zm8yCu$FA}kTr)|Q2py=o+c-h<Z&;c!+<a9e578}w`61x?Jf$)rCjK$7>tmdblW}?Y z(&QGh_jIynU2&7iM*Da_&oT;Ks_YsuVNa!dN*{TT<9oA+3&9tUZ1N%*F(XC0DP{1S zq3r;TY!x#RyPda?&cK}{-_e(=V!v3q-MAmq`LkrK94w<f(oXKx0XrHr%2l8b16n<E zJFB^T-e(ZYP_ruf!823x?z1<Vl1QI)L*jijLjchZWRX+5*+KOW5|r-wB+d%M53ri@ zSza<Z`S1H9(609rP9QUk48&LOX7xe>1RpYN6?li>y8A7CHtW4dX3Ko@6t0KgO`c#z zJGZ-K&)Q&-DghH-zLT|w9N^ttH-GO5dI_=OyKv@*3<s&JjGq+fOJ4n4`w(@*AC68j zwx)wK`!I`3T)Ya*@ElDS{g3evhF))7jpyR(XPX0}MfBuE=|*sT-J(IAb?QHt*)7~s zAtWG47{vq&`x*kk$;8e-K{HYYQcgE!P9`SCdwjMh<|k&YIvQB^HrHiB<78Di>&wV} z%Zux36iL+h@r#pCp!xgDvC7r+T8^Fb!SG(6-^$&u9WH@!4S}kaJZ(m?8iuma(Bnb3 z4||p#%U?bwqco6$I>N$`z06zdi2cifFAz4lI(P$ycj+j2(A+eCex}pxcnjG)laqb( zaJ;^V=`$>TCniFbyAFw|fy0S+)r*8d5STyIIdL(M6r^(xi-u0SuELfD69o6w1S-z- z>O~4b@_kftj316mAZwUJoIv&MDQWfOK3{*HhA^5afMAsIA+cJI>fHiS?A;BgCbqx3 zuCovyKwuu=Q*0C8A7{!xCP`hDPmv>oBkJx;&;Mw@MfH7p@T$dfDt8Ir<=x#i7xevT zN{?BfIv8qfA$x9JzF)78^bz)5s^I#RO`vj)BESjC&NQulxRik}H1?_}2<EFI!dHRf zw_PV>=(N*DMysqR$sRQndzK_>LPo+K|2eRBR_|ap;{j8)tr@=Vf;ZK#`+Zo<IvT{L zt<al3&il<rT)ec@@wgH$Ac=;~!5JQdY&9?xRpewJG?~%ljG$MZ4m0{W7VEhfs6@kB zg`G^9B(jd7EHW5`%t!=W+Jd1RWH|^Y*z#Xx6OiferC^_4_K1(rU263F=+ZCq(a8sh zlw$?Y#?nz!M(~?GB-x)CM)uHkQXt7Q0_UdR&p(gg<WxoWYa;(LXN$JvDBhO}6q^bG zq>0!lkEc4Q2A4@VnFKW8L)0y`yfvh+wTe)z35`>voV3F%22J*#vN<XxuxDH${4l`) z05Fo|(*3o{quKLUs|OK^?DRqc5=r#!AM?^WAZ$yLV6FB^kOkr)r17o2>NcV!)hfbm zwU!r&FDItpfMvYNU=+k#8LT$7jSu*Lp>~sMn~td@7}!q#+9S6M@wpaL;1K!+<9HfZ zZ=X?|TdgfmClrh{4T(lY_lNGs>5SM!Cow1XL$uov&uqFUgq(ggI)-^-lgW9TNYUnV zO|xlH8EB4~gjE+CsWE{lE|QPCLsv4L(#8Uc=n3*%N;@X<5};U9GS$BI_)nnD`b$xX z=H0%Or(Rbm&7;9)eZGZV+aHv}ZxsF&D8;dPk{ST9t`vZt7-(aT{&g&$%F&F>N`57n zTQ3_~8tP5u!B#!M4m=N73WtugyoMA{X1O!r2F1*n?&rzLAbW@#$aO741<WWVjYl=O z`SbbnO{k$*J}3BbxQj|=+C)pG0s8v2|3zNr`uoVYG`}?m1?ho2zB8hRZlT-=ttybN z$u%EB9W!qgE5o1K9ob-Gsw}Qn8o4NZcu)_$SBd?R@~y&y)ecO?*Dr;Q2Fq=iFRs!n zxp4o4>;*LufQ@>EWl+QH{0ZnE?VBoaxhHgygD!&K+`rvjWswODMcxME@_IlUgZrf^ z%>!#tr<^f$*4K339`H_Fq=z%HP0*!ZYxZkR=H?eT_8DTvR8&;gfBZyO%uN(~+(<y0 z4A7M)8QuMpSP~go|K4>O9vIs4aoU@CfTk46PH2a;xH{smL(f&OUr-a1xof8ga|!$7 z%}2GuG3k|jhNx89tcXA>{UB>*tXnt*1Ru+`B<~ADyrv<yc^@0ZC@|b%e%Kpro_VAz z?7&s2XMq~Iakj5!RAJxX29f5439!{X!s`7OXfHDEwz?4Gm9$qKwGk3qN^2(N@wv=J z8RMV)UJaQtX&Lr;mo^Tki!&TnMY5dIZqNRPpWr`_`?EsNmN2DdTgHP(U#q1uKe{zV zIvc#XJ8iEHjPORpwW8fpOW1%sI$esbW^V`O&oEncnzOYDnlk?fa5?e^Nb8;<nnrY^ zNazbOvI3|n1;wd4H>#x&2`Md_!FJp&5@B6<#^BKMsrvqsq@NFi{jn0_g9W!zlLy4Q z?I*)ah4-1v_ld-b5t{3!GcD+$!6mCEn!r;2VD#@prOD%sD9NY1m2S1Az#}SA6SHC= zi})KZxK1&uzq@nsYrfwtUeGa+6=q`g4v-!zWgh4%#{~>XC>~<<)GqRo#n9xGH>IA} z?j9t#sR1-X4V5eM_T(qi>P<yD^2_A@ihwLdEZcmX+2lxhs3`n8-|5&}^b|Q8jIXfv zK2oQ?{ZHu=A`*Avbu;AAJ#shc&5b?B83&g#Uyc3ro*GrMSIPnK`z)355%ab_i~#&* zPli(Ki0JI(FGzb)`Hk}l1h?23OYU^*Z-`893*BleCdlae8-KQ2S#PpL?r+l!caqlU zaA016_1Q?z{Bc%2E{t9ClZg7^fVBy(`9uwU<ChWpnBMR_@rl^$RyJz8vcIXY4^dTz z;U16qW_9~cFS6rMVtI4gtIM{O22W{1T07v`3%k7=59nl6FN#0nZ(N&4<mPVtRAYrs zwhU}Zi8fQiN^(>QBww++AHZJjQ6LHX$Ad<k^gB+|bwZ`>WqRz8BPi&0<KxxO77J=w zi9AK00tNVl?X-4e7Rd}k^DEs9kStp{W_x`X=B|kF-iFi5d>d0NG`b45J{ymg!J~(x z1^vNpm<xWE--jJD<L5mhdpVD`@`G}?@SlsJ)kA0szH_c&lkzjmf60R8Xl^M)71-8t z*D<uES%N!}P&EaJ*%beZ;9<4Gcx9hj3M%nkC4;_mgjV|;o*)^KAw;LPagi8NCQ4dg zGJluf*YbM2{$*Nx60IsNPu_2qFRi)4EmnBgD4^GSe(pX=zMm%4z!%13S75{*k>P|q zw*_x8V|nior59Lhy%eixH0g-zTEQ00`l00?TCidFMI0tfhxah_=_M{yn%dCoRTT)p zcZUK|12-*-b@z2*611F3LN6n-g~b$dQuT*Jw(wX=J(V;6;t4c#7VFM5a0cg{>NHmi z&K`54xFiP#M8Lc$2G*qN)jSpd*NRlZ?35$bf(9h7ai?A1Pxi(vc=z+mfK!a9UnuE0 zD}YrZe0q%yCV6NRECuFuY84266f&qaauWLO=>de}e4Tmlnm??9KyHl-Eyq$6@{9Bn z{Cyb(w++yyx-PkX>a?bH((krr6ap?$4${9{^m7@KW0la}=I`$0WzI&INl(nFJJoCE zpAPx5l4aVH)-Q>fs!AzS5J#}^gxW%=90$D*29XHm$9uY2D$9Kxx-$l@uW*|E8troY z)dvld8e$vy@RbcC6}OJ0!uwZTe$|!#7-shr)5Ng!nj`>YzC0X5&6f+#ch9+0#&QkH z2N#64KhkG9D#L={FHPcsXw*a#aNUh@T{veTyfpow6p<J>stFp0X=W7@ZdHzPH*=)F z!qYr*ufxKXy+!7OIvu!dhIKossW&p^cJJNr%PCw8zL8=ylbCZH7mhh8a}58sjZUFU zTGY2v0$<_3Ed)VmQn{HCVgl1j7!5%c&xNQX#rr?6TDQ^1e~+)A5z-dTc&i|z`;jSQ zyU-Ib)&=<;5gg)A$`&;Pv4NWUgi!w$v&68_2U;C(LQ^0uUM)ymF5X0d?9r1D_KEPQ z2kxDH!y0@A0N*I2+CT)20#H#e09QM+${7s8nO3@CBg<oVxtd_pp68zfWp0(~wyV}% z*2grVB*0o^Q+ai~_=xoWB|JkE;GI`->mjOils4~M*l5ffHrqVTSsfHt?hu<dxvO&J z48;be#<77>M9#8hTS%UB%{i}VR&9OQ9bK$Ffx`={W=0PF&kL(J*SlB*vip8P2wdIe za?ujx{^iu{X6(mV5}Vy(O1V;s6CZ!rSev~>!sFW6dFYzNCP?dVt|{H$fp}=A11le7 zht<f(yxsJIw9EvSqbQ_J!TNt7SG8!hNz?mS@&WK_3Q&A%%}St2ev~qb<R179mQJC6 z7~<DHqzRmb#A$S5WgWqQcr8~a4|$;`c6cW4-UBh-rG_9g^C#&;#zJLtmRT7wFK;%2 zM|^~m{!VgpKn;3GM;V({v@Y>?`;*|@8S59Kr<<6<NnNM((7M;fz772Wqrw2u05T>K zcaWuqygKzUVp5(dovno;c{3K?yr<*y)Ice|DABfZXj~S-FNwGo2+tHka!&*4^jR_3 zw~E$3S6sd;;#9{^l0pu~)1k|3_?L6RKeZFjwQIS|uW|<BpTU)Vj!haHhitoqc-`IP zs@5>fq|dx=4N5RY?LeYVt)a4YG-=qlX8n(Rta>>RnSUrabM3vLiK;K>t|`I^tmL2G zpOvq4j%nmN-H;U>1?bm*uY&6dbBr+nW80?wG9uO1K3i@a^=5|Kdlsqq2kWvK&8TH? z=6$*IvmMtAk-80xFd$m+rp~EXBZng=OqrOM$fs(OCeqoz^Y>=lIV)}O`l>aby?jth zSr=H|VX+{L%FQ+S9|q<zHrF(>-CxZ#87j{^HTx`Zc!zjCw+(M7fFX4SE(E03)QRlO z6se59mbqmx<r`wHbDg7@0;3$0tFyk9fQ{@*7M>RSbT_}3p7TS8tiDPfpT^jzqmubF z`<SFvuGKv^p4Hwz)0=a+!R$X4noh?UMKt}LOIzo$#`W60{)6_d8}7;6hpU^x3@E<s z3S1fBdfVB0AXfLrFW3bARto+1z>-s#Mv5ma8y)NMmu~K4<g%JG8FQZ!E8IDc5mg%3 zlY46D)Rj`TR+isUkIGVN&VTw4rT;|@9NYH*D@>dCbf3WxEp4yM`&JOOLMR#F21}FA zsAD6Wg1X)uwe~pyv>}dBfyUNQZgZIsyqfAW20c0mDQRL-FU_VQPdBk`<%@`Dz<|yL z`7tp@q11*ZJ)ccVKky_es-~2a_Pb&FP@AM5$v4;yUHD!4pfcE=PZvCo;7-ZNPPhcN z;094Ol-$oh5M?ZgBR96w5~G5%?nXu1X98uC59s0?C-GOY{qpJ?9pfLJve#IgJ=y(z zv653p$8^oxxr#>9o7<tw$T@**FtGE*H2l%(Pn%~4LM{@K57DUc!_~TSn;c;omf3xf zfuF8t2|$sDN1vlpi_@`rFbtv|%8%S_+g%N-)~>S?>B{z9oOB*z6;OV~a|B<4+1v8# z=;}uuhhwTSec9ALi%Yy;?8qGvHeU{JPT_Hb>JZwPIWnXvTTqmM)H+sC#}<I*QpOI` z#|Q4AC@b6M1?;ikYh@Z8FdrEVP0AXkPaSm;rIbLLrNP<liG==eX;fB!bB}cMqfWZz zv0+WQA50z;1)u$<{s0i1nJHyZQRgdJK0B0gA{<U&USAxt`WvO4Lu%V;`LU*z7*5@G z<mJ*>+a@m|j5|&)uw3Or*HKA!TnW7@i-F~CjSMR}4lte=`PFhxiJ%&{YmDdQ2x!UP z)Ps%Gyqk(taIT~iu@h*ojbWCv(L@ejJZS>)8CA0&EeX8Eb85Ze>BnQaBhB1ypbXFd zM$5PLpa5A1KjT*=NqJeb&gk(p3-HYEDEd6~&>$Y{=Rtzy=TZD4X5u=|gz;mlpGT{E zF1w#&&Z%v|Y0hE;68?>Y5J1?J&+`r+Jp%A0%TJ0aCx7HEX#cb>qgYS|Yg*_4S;Vdf zm$A&O|2h3gQzq@e#{!9wap3znjY75`_!_V8I4yTFwGBp?(Yhi=-#KCyyq$uiW~khH zC?*yUm_KQ0zsUi*(Ahui0*!bRm3)U8LYuzY+2dNUXY|q^5f)hyC-kToSE_)J7u|s= z#%a{GHEC?YEh!!&HS|eoCVr^Bh6ldMT0|(BmNfQ$arXz2&xd!y8n-ciQkXabBJBAf z<Kn+Uhm%owkm0AD`E^)p7Lh(*$c6bGY|)SgjHu<5z0R&)9{B)G?{cLQdP4*xYzB2x z1o5wJzne7hz^Azf1>W=JIZdS&)4K6k!}NfQvt#)IB@el%K?_R#I1A5+g1kN4pMpmL zK8ComLiEqd%P^F%)#(LXK(vC?`jp_~0AlL7CqjN-aL?=es{Jp3>A+D6SzU+L&C->4 zLfGnC(*=GAY!`H)xsd4zhi<o^nPJCcsR4;hmMbo7J?Z`%b;N}edPktZF2#PowX^K~ zc^7<7WgUme0;qdSI`g^}aH3Xp3$ZIrND%tl11$c1YvFo9GJe!m+<d701^J%{D)gd2 zxBC}{FLt0r&c{Dbf41^XM0)OH+Mbpaj0PL$FI()>-T^G1zF1;@OwktpjpS5zozpav z2rl1S&y<b2yJpQeQaM>PI%Bvp1VIU{of=$JVAiW??MFs(CKE*m`n`h+<)#|jsw-pE zS+f4m%SI93^a608N&S0mXI=#jhx%z`gilNhpcWj=?`8hu9S*MoKAWNl8j1OD76+Oh zJPz&rzpMny0#$#r7f;`+1ixUX&RHtpJDkF$PR^h6O*b3ePVlWmrZU?IYr!_P`+jql z_WS>=QRO^hUT9E|Xh5BKLzHHv#+)u5Aw>u=KGig@n(s9Aqj_m|lWZVCSWuF{2i}@D zagh-#0+KTtzwg4zrq~^M29L&st-GJJ)q%ibJ~;llSyD5Cb6AppnueWAJJW^sM;MFb zmY4YmM12+WivkeUA7fsS>5xSKmYq~Z(gDGx*{~K(*e(TGTouch8L{#LLHIhUI2CaJ zSb7051v=lXD~Ka38f4G;v41;hSzobTDPIZ_)^@-Dy^e&{E!8O8hs5%_U2xxlyT1Ji z9YIGb{_Qt5G4p?O`?r4XtNmRuqHDi)2FfB=y}wumD(Naxaqf+wZkG(xM76bZuE+gG zm%K>VE&+P%C4*qdEYNI3-H#p9-yU!XD{;rMKwE?dr>DP@#$I4DS-aS1fw1K|s`30v zM8m^+mS=}>b{s{cp;=pXA~u!V{`u-<GmC9063X9W_7{h6Noh&nX}cJ-lU>btS(z2W z<ahh*_bPQ2>&Y8+I9~O7D_|<oC2--r%p`%N>k2_x`<gvN8+YazzOw4K@b}B@AD;=n zDc+UumSP*=Z<36cCo>91puT~)(0elJ53qOks=PJnV5zh55uuV$`9BlI-8vwW9D`Yc z8R_dA@HX>5hnCv74Qtb!c<8hkQ>hQ1o+Be(#msA^^m^Dj)Sk{f6T*{;VrRKN0EEKR zMI7!20XxIZnq&mfMT|iT1kZ@01It5%y&Qi);2373)ndLino(#))Ctd3mz}<Q6(M3Y z2?tP+4|H6Nc=R29v-9$zgxv_;TXtoJ_ylrXaMP13Fbmg8rq0D(>lEsWqmUoxWQo;B zB0_?dD1;V$1n|fW8&(-RW-e_BiS}7CRko0l{^U6R*-7E`CVaU-ry^7h`dVCpkE#@m z8$j(GT0~-eS$*jV@Il+z4&$9jJye}svyQ9R6~8xiqvUUQE8m3KB8elKpQ~s07gwi% zGYkSU)C1)K2XEU;MDeh?dwj-gM9OCuv^9u<XwPx+t2*a7+_Ds=4s6p?pa%NaRB0;D zrVQ53w~F7n+AsizP1g2P4k2*aKbR$pL*<?t2fi+g@Xhld(9igtI)hd~&#pY*IuH1e zMO_|9!FAai5wEmZ`)(fW_5k6Sw2kfcUr`+4pMDeOS)PWnXqhM_1sKh#9UhM+zgxHF z$e=zL_c_4h+zSw=M7WG3d5r4e3FZo-tv%G3&Z(nc(j!loCltx5PF&jBF=JZQBAJ4H zP@8XiXPG&x70ma@75Qr>ZhJmbu*K>U7Aoa(j8PV4hH&}mc{qK3Z&Y2SsnN#}@MPBb zhDL);Jwd*Ko5gb7aNMgkaBlnSaJ-;X69lB-!)Ij{Vn<VRcF2?9p|FYFGH;(>Ywyyz z5&Yb;i>78Aed&PoB-ab}VLYiwzO=xg7OU_il6)&f00^ZdhUOLen3M-pg={+wCE>3g zT%h=Zr_c1i@1n9b*q3@GH<*<0k$Zu06WV=*$m^R&PU~X0$ipEV<9Fqc7RG1%P~-RP z%ZqBPjTM>qeEs`7hG;p&dbqOS6HRX5bcup|X?UKU4&ONYA{wN}3ZJ8q#fgLyqn18B zw-+NvdyG<JTL!#q#}uV~8z#TY?Iu*u@D6o&lkLd51QKOFJCm&<hR~%CI~O;X=hBKR zcEv+fl^<`6VR8`a`KPhT!fIkOpx(=ko8J=z9Mg;!a_Prc<K*>67%m9yrJP?e>ons2 z2mLQGKmB3ZO7vhWX74!GqAL!n2-RwVCey(0NOFl*uvGvPVke|d!_pIa%C)>HtD^pE zX4i8Of51}#qPBzQ<vqKwJQkAAivB7iM7}>(3@!KaN%BW}lbB<JH;q_MoX(B~3_YZ2 z%2VsiKl5F1g&~SF#+swxA|~>OEX}B00^!MaZy!)Th@I~qr4dXF8^|<kE$2D&1}IoR zZxzZb>#9E9XNJzV<}lPt9Dw!RCFKPL?{D%kEL@7Vj(qD(wL*J-pFkJ9##bk;%pna< ze$!PI()tq!GKSw@nTk=$ee^8#6B8s!YIuaF<y7|pV8z-SCgqM^Yz3gANU1qTSx#dU zOF!u#T6~IDDn!CMbrQ_s7?_`Tl-zeVMt}X^(Qp{WE_vqV0!ONyO(wX!nlx35r6srq zV*EDfoJerfVC^y*c=9?Cj&(11j^C^uMEOms87nerbN@1F?xau#%-af`k~Y)N0l_`W zcJSn%&-(-Op>1m`&}}2Ok&Ku}d})Oebb~>|Y%5U0*xe82T&!#JY`OScEc3d^5cT_S zs`ZOLe_iAn5!3m+LRm@b$p(L0<B!se*^aaJpW;tiyE}pz*zo}e6#kXiY%OXvC1Kv; zhkZ<Mmii3M+;D9xv+p5~YwhbynkJh-DzR$hS5&<--p}wsk}fnve1h&iWrnXN`MT-| zqk`R^303_iEPpALW1^~%v#nQ9K2_-7X$(b*2aM>B$nL6p?@awG`<iG6s4wfsbkl-T z10&4IB1NvW0q%;qodpmZBs2&U@sS9)eGD!+^(zks#|q~G2JeXR^E&PdsOjRK^FF$b z;T5igSvE>%d*(BTh@xq%-O1LH%MvmdU2t@_FCFKmXf;mvpotCKnqTlVSs<@Zya*P^ zbqW9b6@B#aPAw8PY6(yA4qmlS_+|TWuI0D{=T#@xONNgAbAa22tg8w=dpo~cswFJQ zfT>KAn6}5BoxpKg<t+BvZhU5}Pl*5_?;SYvFwxBGERGLER_D(=_^v!ILF*`_M}*ns zo5dLF2saYiUBXADFPld#VYqtT(rDPD*{Q$im5uR6*Fn~6W&ME)wdG`OniD;=UqZU= zxN|0pc?f=7rer|P4?R@}A<G*+;48VOS^KQx4gw<cIA9cE{#TJqZBw9(#8XB|$H?UR ziHYT4-MM3gYE65;@x~thBLA*Y5+_bQ*N^W%$F{|2VaqLQ!fB4adLjVpVW;qKZv zW;p;ka#CN+jTT0%Tp>=%u~#JaKK)`@TIiOxziQvG$)E_sVB~iu&(Axq5{i>zi9pj> zyiR>6Py*uhzJM0zC)Qtm`@~2jAm4gze3m0W9MgblH5z1Wdz9j%<pW(~li?j#=Lnn? zY4aB&b){y8%eMX3<06yUjBLq2b-T?YLNgY;<QjdG=MXP&WZQ;A$wHDbTi@UuW|v2f zUG)7=3pAgSenucWZh$X}QOcu_T1bev=*yERe&F|Nph|4Bnv@!fVm>nF{0>}wt{L3| zgnIR24p*GIh@^VV&!HC_naWh_d)6S<slN&vgjuy}@j^ekE4<&(g!g(A^uwQZccDmb zlZ0`|yAt!I&i2hdF(k#HYv4H5_EnBN$(|1i9JS^<q_-+<{gxyhW3i(9KZehIPg2SS zIxKVAfoF(-L+dDq?}HSLO8lTfB?C38T2^^-e)O1fksJZrNLtaeBqbS%Yh6UqMCq;8 zzw!<b4;}kbQb85pWPBZ++gH|x1$oNrZS6a($VtK%)FeV$R+hNhSL9f8MI2}ZJW1@> z1&&e{JV)&$<amL$g@>ue$`|{46-||hV~^KQ@_&>G2whVsPv)27Kh^{nI}!!~j93}& zz@P2=7l`m$KnFY{go%%&yO$vrjgip?X-IGqAzR)@sSRw>y2g8H6;Cj#VMK9K*hn=* z<ibBWEsWB7R+k|<h@is^ATt70tl%V-2*(ROY$>w&D?LrJ?%>1xUdk_y82oOzeFZNI zMAdjxNN|p1Mea#EB$x^$g9{pbS{NS9M)6;Po#Jdc7L*Y0YpVlK+85F9tdM4aefD*k zPVg(e&A%j~Hz2mscgn#TW?hYi<Ml>C<6XnOg3&8?k1NSdgaKhr`L#ESY7bJEy&FP0 z4dcOe{#6}+;bE3N+fL=PC4sseg2*$vBzc7wC(-$AjzrTI-_chxkLIeUCz#-IUyuUn zw8n|eU+|$3%JK;jlE;|0w4dlKh;akY+!DbityF_Dtin$}*O08_TOhJawrw*ekZ~Lh zD+&bN9Gu9tW{Lzl*?qcPJIr-OqgNys*VMWDfY1g5m*_CX-nVLsjuSZYRm9<-KXB>F z^vzrtR?a{rA53l0bf4|AfJvWmD-mK$kURxP(&B_PP)Ydn{$^dB4PoLdI*ERpOThge zd`F_I2KD(9!8|)OhsBzwLn{ceow@evr&Z)LDCBeYK@TVbB)bvmAJdP82SyM`)qZU^ z&R=m~G;W4~2=jlyGqN=*YBo$eX9c(@+Dn+a0cQ&2WW+e>ty$~5k5Uqt$EEXvYI@>^ ze|H?pv@<#*?VCsA!L%TsQnRNq@io0G)~9&G5-uTauBdIm9!ECb<H|PtkYqnbiTziB zOK)53xz)A=`||pwf^(KV*Y6L6()s!xzrj91+p_41nr0GDMFfYk2*1~DdFb#fPXVVG zX9c-NrubD*XrU~@LP3P&_y+_X1g!G32$VJVcWP$eyPp7p8At44Z0{DGOICqXJM>iH z&co1TKV<Z+`Cj0hH&@LUB944R4!G9R=D}?McvRGHb~0j(I<{f~5CgH0khK)A%59sW zBgnEbFV)dYvlyskk{che02?Prwh~M*!^_`ZPoyBQbM7T#E;th|xZv1yZAs{{dl!r~ zU7e$NBL%FS-V*0svB}w1Z3}%L8YTvzDSZ2N%TcE6`f}1ikHH)Q17oVntGr^$;rT-v zj?^=VZkct*(!N0Bd&bH&M||kq6||}&HB;ry#?=Ckn;<_2-q)m2J|OY*_l9u$LRK=Z zeEOctgE?icq6k_s$D*OfWV_Qz)=)w5!ME3=c6c74PEM>lZ#**$E-AhWf7xh^)(F=; z27=x&wpxb9?|Xw|Gli5cb#qAaMl7tF24B~4>V-*Ei+MW<`${-(c1F3vkAC)-aKR=R zkOxdWh+A14_m%}`s9XJ<{oZ5)HGUzxLlF0|+%jdh>#zkCVw#`Od}TyI8{A6c@d9Uv z0JB~!$TcahpLPK1S|x&);95GmcXm@ZCiyIWuQ*gjswkUS8}f}wAVV6#V;APVv|*R# zIin9${p88OiAo(QEO<QgrhWwt;`n!Vq;eIt%~0l(HYx=w<s&^w#b<Wt-6SO(*{n}^ z#WociD<nJi#s|u8W)$hbA<3xW!Yg6F9gPNfV|-5Fx_y-0FFyb*1??0Uwp;}P#f)u* zDh3m*##Av$f-dDA69N=)-G}Q?LSWV!#ttz%AGwiT-40frGChIn5xsBsnGsgtIv$-l zQ($ijlu`vLoZFpk!J;ZnTt;qKJY;I?anAzS+K--omHcdW><RrS@}zHSvMOgF<lQMB zoq02cMSI3AJL8XglKX0YXf&IVB=GSU#qfY7YBG{)_lrTF;vct3cgQbZJ0iP^Xf%7f z5#I9ug9?PAFprsos>fvQCLtn?nG75&btPs&6####HYx@jZR3l}HnN@6Fll;w;Honf zjgoouHpc=|d>@eLd0g6tdOkru6FDQnwki?Cuc+q^Exr)Th)Xxd2-qfa{+!*u&v~gC z_Rzu~XAYl{d0^VUq27iNGj8jU?Be-P)8=|=c=`)%Z|S-oUeB)@5m(Q)DQwT(KDQ-x zI<HTVgf5b;){AR%8(Zxcuvz1Le|+S<Vw6MC@+2Xb#xz-Bp;p<@2D7>_j*w^M*C}=j zCLw2<+5qAbuqE?Jw3>Xgr3aJfIlurzBp+yDxRy@`oQSO!W<%d$=9rE~<^$4tF4zT3 zDkd--rs028-B4m{Q*obqmL<iqrD!LgVOLwtP$Qj+icAw=ukj}ywlS)9we+P2O1Stj zkMc!JneAh4)ZLw8&v4Ya55$L!X~%_ErdCk>Le6qz#+~RKhcRP__Ag+0^@@AbG?5YM zDr9w9!*pQ&z!WZ}@{!JkCh?Pn-_yV$1zmJNc+SImJ1^*3U0?rVQ?()$oj|<Z+-l-! zMmCP%Au=MoF(YR+ZX?0<l;o#AFuN7*y*Pl~X4v!B;ub!0_hhi~?g3?ClKFuev-XxQ z^s5Wu?YlUYIQ}{L^~Su#>|=6meJH&*?G;h;y2NRfg2y0C!+|kJMQv}s%sE;9n+c~P ziB_SQ;qL)7y*W(oGf3gwTngxBcx{|P0t!+CE@!=(;h<#p6!bi}Uw&tfc91+Qn)PO) zrr`wwTLzU7^SFXm7P)#2?d+pXGm!S$hMDZpj;#<<7?B3!s^?o5UR4DA!W5im(PQA( z0tI9x7xeb4_1TU>IL-D<VGfl%AATUh@io;|sp)y>mcbS^TP%O7(Ul$mn_WZ88(5_p zz>dKIW5#LKy8IT2VkE<IN5od5`b+<d28R8b(qcOf7BdMX9Eo6gV*l{q7*II!_;Q>a zunBJ>6w<ilELmW-qqWJUfNS3}=bYO!*4gs!!|oxs$1hOG5jXBq^jScJ=33g$v92p6 zpW8dy?tYOFDuo84;TqpNrX1dT$uhxn-t{3_8j14?GH(?Y8p)t3yAgAvP`Z2A{chCh z;t%0}8UZ1!xdRe@0bh>56AXaCakkss!n(hBy8_nkk-CipnpMF3CA1oxu=vsz6Wr*B z1Q4<++H<A7#6L7!P=h|b%LqGrG0&dr5~xxGWa4brW>(T0u6V9zvkUBiuCV4BTs4}g zqD{A<7!JZ28XO}ZXn0$}LxgW`g|-K#^0g(8cS_gyNZK`(`)_4UKcMN20Dt|$$UP_r zXA&tv6gxtWx%JuU^TzW<MPDX6nNCz5OHKLyQATk9rCWyB>x~o(8k**4*DugRJ__~q z78vY=Zg*Z&K|=!4DwsQc@6&QCn7fAyH1J|(B#)7bPD#QrOHOQUVu9)8wETeXPOYSX zZ?Cfr=-1XvVJ|;QMXda?qf_nKE4Rf*&iiKDR8cjk`4V^HNhYiCnxY=|&H&Y5P=2n( zpCogW=FVM1v_irkxNLqY|9K5-ud_Q)(2<+BE#4V@Tv|E^Z3ucqa9R$c9?L|(K(1<* zolo#Mo{${*o8v?5e8?i*IjWMnv$|WW51jbjAuvse5OU}>dfDyZrD=`gmUfhjwxRx3 z&P?@~Rtcp%f>XzBzN*4tquasB7@C7M+*22E4l21%S8wG!L!$g3>2eF3-l}#liJ-C< zR_QP22+NKu<(v6Q!M`|9vIvZ|SaUB-P69Pn-VUfSqa}xvwl;C-xK{(fME;GU6}A^u zef_^<J~GcA0B}p;_F`}*rwN1_9X<bwwJAuyLxR}4J!Ad9nX#<8->kpunl3kEZp2w^ z4Us|PI3b@Iqtm}Cnzxs~_dh9^XKM5=A~tuo_Y90>2o6|L$nZ5YEjo)XG6+kZ8dh++ zL4hZo0`Q`YxK+p`Y?n^iuG%ED;&F(A@#)e*1=WG3Dj&e&g{K)%4sz6!mBTG}_r^F{ z22NZ&6fMV0pJs`yYd1RnRv>Wp2D(%ix>iV3C-cgK#AmW9;-CoR3PKu-h^P|2rTc?) z5+T&1!OQQlzNfyINgO?>lo>m>sgVu@R>0%v|0dopLHr4@bW1FsiXr$4q8B%;D@U9e zX=eRTPs$iUInrAR<d{L9BwkC^6HWd#hkg<y94gC^MNquoZkESqE!Qrn?swq<LCJWE zoT!QGb;yW7o0mDR)or@zC)Wowqy9TY-BB}Lml226Y;(V>RW-`!lnIMpU3N`8Bd|GA zfVW(3?+0O)x`HYB`ZRG9$=cTqvVACu&#c5^jk`9L+7eE;+fO2D7!|~qsWpOi8;n_x zL<t-+Oi&HUdSdZDCg2<uNO(`HqGZQ9exdN{yvY3b^!(((tmP8SwzRyuqr>aXy#>pC zma6rF#{3UB=M?fcr(JxRK<2t}ucm~!PU7t_*(7b>1Kg0V@yx*B?0F~G5gB{?l6gU% zrAu1%<L!P#G*g|Q+06nHNsIhso1ce++E;ftzU`D-dlq_0#CB;pC9H_C0ZYIcnO#@M zx(OgImAj2-(U)uEw#c=d^p}2kIR$Cm7D~Hj`*+!CE7|qw?I*|urWDUfclmq%)}(>z zPg7+uV`HtQM|f=?f1QK8Enm8Uv3cexit3+=Q4teZ(0tKoiycDd8K`T)NLY63ezX2$ zy<-N`O+P{$8CAvE2`)H>JCHOvg>MPcJHCpD3S%V8%_IJ$c)#s5x}0JWY^~&-xeQds zfBP%;BLsSGIs2!mPX%4~?_DdxpAO;8X%tpwj}p<oOp@!7()={t6UFPlbe@EHLZG#V znIvjFG!{%T-u&w}lk=N=4+pm<$G77~$LPmPB3p;LwJyOLXBL~|2zA!kOgQf)WMiD$ zc##>xfDg1tN|%&Pw<xcUxo6#Fg{MGpH7?pJGA=x1FAy01==CY)MjV<;U!E%UOHmJP z4(lhNSeYoaV#vsUo{5BQ=;y5+$F*_z!V?%F!lEnJJVbaj`6f&uv3TImn%ku#hA1oj zb?)FamYj_CWITgBXDugyZvX(w&XC%9+-L07M||30HTxwk#q*Ch@x=3p@53%B+m8Qz zV!V)<PZ5F1;gf@eASgl^x5=Fdg0-$AQLV=nRda{yvM7?HX8>gD5>Sh*M>+x$nsz6m zG?n{t*f64w6O!4CDe}GBJp#=DBskiNqSW-`?7NmqmZ?rh60REc?XR>_fhQ^T4s?^J zUVpPk(tOcfu(%FVTvM(`4R9d~JSs-!{7Q@D9lfXJkM?XnJI~<r68J*%_ClOaeLZp# z&NC83LC>AKy5}Mg{hP7`{)=>mVIBr0_UekZZgYJ;OJ&A-aAD1qyzz;OIuR~Vri>X5 z9={)<Qc<|M`v$bBov#XoiXLyu375(Jh>|E>Rrln`issQYfJ>~$-sW>G%~o-!c!X3{ z4m<3V=<X=}d!l)TL?!?W4m*5H72eYpiWZrH703O~^6{OFT>{OkR-<wUuWW(%bx2k8 zYD-A#FO`oX4p~y6R!kQr!iqwipNz-;Y4`Pq$TO>5ZI}%v*(mxxZXqZ+-CY>w4k$m^ zfF(_8y>SsXR8+YBUQI@&`k9@V+6)WrP`Ad#Sp)ia-2`vY4;2UMVLABm<}>!S#Z#b5 zTfS>B56Y;M5-9GKeDDfi);9X8`N@6}52-u+P_=7F24v||H5gCeKGClRG)P`@?&u(0 z&~oN+!Dd*+ERtmpq#fTB=X?~Xd?(S7&k(mURfON*$3faNyGyl7!7QUIlBF&W1TfMf zwC8sJTL)ys)kbB0=5D#oc{k4q^_{bCah5ryOAQBh6|c+jkp-pQg=b{(EaOBscm5gO z`Xf*Jkxw=Q5zO=oU75vp-|?kCQqu>?xFtf5uNhTb*B~iv82iOr)eF4Lj-2RP^MQCv z^C_gIh_i?5`jmj9Cr7dM9K|Q>t#adN;;exV5 w3nx#|kz{Z7rQ+kO6Fc`WooZc& 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 JthyZbe*lzx1>67t From 5090ad5c41486bdb78aaec63c4578c6d613f60ff Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 31 Jan 2017 10:21:00 +0000 Subject: [PATCH 177/709] update feymp test image minor pixel change in position of labels in texlive 2016 --- .../fixtures/examples/feynmp/output.pdf | Bin 6290 -> 3668 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/feynmp/output.pdf b/test/acceptance/fixtures/examples/feynmp/output.pdf index 5a93452b0e3cb6664f792fc971a3011709c9d187..9428ca938518bd1cad467bb61106db1084b6ff9f 100644 GIT binary patch literal 3668 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 literal 6290 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 H;mm&nd$@)w From e25ebd296e4027ab9b6ba9193719b5134de9e221 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 31 Jan 2017 10:40:05 +0000 Subject: [PATCH 178/709] add debugging to acceptance tests --- test/acceptance/coffee/ExampleDocumentTests.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index f7f82ac1..23c3407b 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -23,7 +23,7 @@ compare = (originalPath, generatedPath, callback = (error, same) ->) -> if stderr.trim() == "0 (0)" callback null, true else - console.log stderr + console.log "compare result", stderr callback null, false compareMultiplePages = (project_id, callback = (error) ->) -> @@ -72,11 +72,13 @@ describe "Example Documents", -> it "should generate the correct pdf", (done) -> Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => + console.log "DEBUG: error", error, "body", body pdf = Client.getOutputFile body, "pdf" downloadAndComparePdf(@project_id, example_dir, pdf.url, done) it "should generate the correct pdf on the second run as well", (done) -> Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => + console.log "DEBUG: error", error, "body", body pdf = Client.getOutputFile body, "pdf" downloadAndComparePdf(@project_id, example_dir, pdf.url, done) From 444b3586a7b8587992e2c37cf8b6facae4d4907c Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 31 Jan 2017 10:47:49 +0000 Subject: [PATCH 179/709] increase debugging in acceptance tests --- test/acceptance/coffee/ExampleDocumentTests.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 23c3407b..6c9e96bf 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -72,13 +72,15 @@ describe "Example Documents", -> it "should generate the correct pdf", (done) -> Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - console.log "DEBUG: error", error, "body", body + if body?.compile?.status is "failure" + console.log "DEBUG: error", error, "body", JSON.stringify(body) pdf = Client.getOutputFile body, "pdf" downloadAndComparePdf(@project_id, example_dir, pdf.url, done) it "should generate the correct pdf on the second run as well", (done) -> Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - console.log "DEBUG: error", error, "body", body + if body?.compile?.status is "failure" + console.log "DEBUG: error", error, "body", JSON.stringify(body) pdf = Client.getOutputFile body, "pdf" downloadAndComparePdf(@project_id, example_dir, pdf.url, done) From e507bd6394ed1d07272117d87e0bb90dc29fffc4 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 31 Jan 2017 16:04:59 +0000 Subject: [PATCH 180/709] update acceptance test image for lualatex small pixel-level change in output --- .../examples/lualatex_compiler/output.pdf | Bin 11405 -> 3183 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/lualatex_compiler/output.pdf b/test/acceptance/fixtures/examples/lualatex_compiler/output.pdf index 38ce5418e0b8df9a51b75fd412138a058e8d7be3..3dae1ad270e019a55f1fa151ee8b024fbca9c69e 100644 GIT binary patch literal 3183 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 TFP-EJfUBV40BLDmGaTSwQUHgt delta 11123 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 LC@3V9B;o!qKy58} From 20cb52793de9709069f9cc0c0a105ade114fc2cc Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 3 Feb 2017 15:16:47 +0000 Subject: [PATCH 181/709] add acceptance test for hebrew --- .../fixtures/examples/hebrew/main.tex | 14 ++++++++++++++ .../fixtures/examples/hebrew/output.pdf | Bin 0 -> 24169 bytes 2 files changed, 14 insertions(+) create mode 100644 test/acceptance/fixtures/examples/hebrew/main.tex create mode 100644 test/acceptance/fixtures/examples/hebrew/output.pdf diff --git a/test/acceptance/fixtures/examples/hebrew/main.tex b/test/acceptance/fixtures/examples/hebrew/main.tex new file mode 100644 index 00000000..0eb48d91 --- /dev/null +++ b/test/acceptance/fixtures/examples/hebrew/main.tex @@ -0,0 +1,14 @@ +\documentclass{article} +\usepackage[utf8x]{inputenc} +\usepackage[hebrew,english]{babel} + +\begin{document} +\selectlanguage{hebrew} + + כדי לכתוב משהו באנגלית חייבים להשתמש במקרו הבא וכאן + + ממשיכים לכתוב בעברית. טקסט נוסחאות תמיד יהיה בכיוון שמאל-לימין + +\selectlanguage{english} +This is a test. +\end{document} diff --git a/test/acceptance/fixtures/examples/hebrew/output.pdf b/test/acceptance/fixtures/examples/hebrew/output.pdf new file mode 100644 index 0000000000000000000000000000000000000000..09767bd88d4b00752d79ca5a5fd7acd828004416 GIT binary patch literal 24169 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 V)%k5x*x0!_LC928;))W;{|DH$sn7rb literal 0 HcmV?d00001 From 819c642b8dccd8ef8b952c41e8a7c028efbeaf6a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 3 Feb 2017 15:38:06 +0000 Subject: [PATCH 182/709] add knitr utf8 acceptance test --- .../fixtures/examples/knitr_utf8/main.Rtex | 35 ++++++++++++++++++ .../fixtures/examples/knitr_utf8/output.pdf | Bin 0 -> 62993 bytes 2 files changed, 35 insertions(+) create mode 100644 test/acceptance/fixtures/examples/knitr_utf8/main.Rtex create mode 100644 test/acceptance/fixtures/examples/knitr_utf8/output.pdf diff --git a/test/acceptance/fixtures/examples/knitr_utf8/main.Rtex b/test/acceptance/fixtures/examples/knitr_utf8/main.Rtex new file mode 100644 index 00000000..29d575e9 --- /dev/null +++ b/test/acceptance/fixtures/examples/knitr_utf8/main.Rtex @@ -0,0 +1,35 @@ +\documentclass{article} +\usepackage[utf8]{inputenc} +\usepackage[spanish]{babel} + +\begin{document} + +\tableofcontents + +\vspace{2cm} %Add a 2cm space + +\begin{abstract} +Este es un breve resumen del contenido del +documento escrito en español. +\end{abstract} + +\section{Sección Introductoria} +Esta es la primera sección, podemos agregar +algunos elementos adicionales y todo será +escrito correctamente. Más aún, si una palabra +es demaciado larga y tiene que ser truncada, +babel tratará de truncarla correctamente +dependiendo del idioma. + +\section{Sección con teoremas} +Esta sección es para ver que pasa con los comandos +que definen texto + +%% chunk options: cache this chunk +%% begin.rcode my-cache, cache=TRUE +% set.seed(123) +% x = runif(10) +% sd(x) # standard deviation +%% end.rcode + +\end{document} diff --git a/test/acceptance/fixtures/examples/knitr_utf8/output.pdf b/test/acceptance/fixtures/examples/knitr_utf8/output.pdf new file mode 100644 index 0000000000000000000000000000000000000000..49261377fac66bb2c12242dbb450f0cfcf8380cf GIT binary patch literal 62993 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 RorRs14UU{#OhFv({{hobEV=*y literal 0 HcmV?d00001 From 71fb15e0ee6444a4a918fc7451dc3c158c1c79b2 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 6 Feb 2017 16:27:47 +0000 Subject: [PATCH 183/709] update knitr_utf acceptance test output needs to include table of contents from multiple latexmk runs --- .../fixtures/examples/knitr_utf8/output.pdf | Bin 62993 -> 75114 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/knitr_utf8/output.pdf b/test/acceptance/fixtures/examples/knitr_utf8/output.pdf index 49261377fac66bb2c12242dbb450f0cfcf8380cf..015941cc2019cf51de6b6405eb5cea68829d06ef 100644 GIT binary patch literal 75114 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 literal 62993 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 RorRs14UU{#OhFv({{hobEV=*y From 5705455ce1f1e2fd688cd2fc4c534829593561cd Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 7 Feb 2017 08:12:39 +0000 Subject: [PATCH 184/709] added acceptance test for tikz-feynman --- .../fixtures/examples/tikz_feynman/main.tex | 65 ++++++++++++++++++ .../examples/tikz_feynman/options.json | 3 + .../fixtures/examples/tikz_feynman/output.pdf | Bin 0 -> 34780 bytes 3 files changed, 68 insertions(+) create mode 100644 test/acceptance/fixtures/examples/tikz_feynman/main.tex create mode 100644 test/acceptance/fixtures/examples/tikz_feynman/options.json create mode 100644 test/acceptance/fixtures/examples/tikz_feynman/output.pdf diff --git a/test/acceptance/fixtures/examples/tikz_feynman/main.tex b/test/acceptance/fixtures/examples/tikz_feynman/main.tex new file mode 100644 index 00000000..c7520685 --- /dev/null +++ b/test/acceptance/fixtures/examples/tikz_feynman/main.tex @@ -0,0 +1,65 @@ +\documentclass[tikz]{standalone} + +\usepackage[compat=1.1.0]{tikz-feynman} + +\begin{document} +\feynmandiagram [horizontal=a to b] { + i1 -- [fermion] a -- [fermion] i2, + a -- [photon] b, + f1 -- [fermion] b -- [fermion] f2, +}; + +\feynmandiagram [horizontal=a to b] { + i1 [particle=\(e^{-}\)] -- [fermion] a -- [fermion] i2 [particle=\(e^{+}\)], + a -- [photon, edge label=\(\gamma\), momentum'=\(k\)] b, + f1 [particle=\(\mu^{+}\)] -- [fermion] b -- [fermion] f2 [particle=\(\mu^{-}\)], +}; + +\feynmandiagram [large, vertical=e to f] { + a -- [fermion] b -- [photon, momentum=\(k\)] c -- [fermion] d, + b -- [fermion, momentum'=\(p_{1}\)] e -- [fermion, momentum'=\(p_{2}\)] c, + e -- [gluon] f, + h -- [fermion] f -- [fermion] i, +}; + +\begin{tikzpicture} + \begin{feynman} + \vertex (a1) {\(\overline b\)}; + \vertex[right=1cm of a1] (a2); + \vertex[right=1cm of a2] (a3); + \vertex[right=1cm of a3] (a4) {\(b\)}; + \vertex[right=1cm of a4] (a5); + \vertex[right=2cm of a5] (a6) {\(u\)}; + + \vertex[below=2em of a1] (b1) {\(d\)}; + \vertex[right=1cm of b1] (b2); + \vertex[right=1cm of b2] (b3); + \vertex[right=1cm of b3] (b4) {\(\overline d\)}; + \vertex[below=2em of a6] (b5) {\(\overline d\)}; + + \vertex[above=of a6] (c1) {\(\overline u\)}; + \vertex[above=2em of c1] (c3) {\(d\)}; + \vertex at ($(c1)!0.5!(c3) - (1cm, 0)$) (c2); + + \diagram* { + {[edges=fermion] + (b1) -- (b2) -- (a2) -- (a1), + (b5) -- (b4) -- (b3) -- (a3) -- (a4) -- (a5) -- (a6), + }, + (a2) -- [boson, edge label=\(W\)] (a3), + (b2) -- [boson, edge label'=\(W\)] (b3), + + (c1) -- [fermion, out=180, in=-45] (c2) -- [fermion, out=45, in=180] (c3), + (a5) -- [boson, bend left, edge label=\(W^{-}\)] (c2), + }; + + \draw [decoration={brace}, decorate] (b1.south west) -- (a1.north west) + node [pos=0.5, left] {\(B^{0}\)}; + \draw [decoration={brace}, decorate] (c3.north east) -- (c1.south east) + node [pos=0.5, right] {\(\pi^{-}\)}; + \draw [decoration={brace}, decorate] (a6.north east) -- (b5.south east) + node [pos=0.5, right] {\(\pi^{+}\)}; + \end{feynman} +\end{tikzpicture} + +\end{document} diff --git a/test/acceptance/fixtures/examples/tikz_feynman/options.json b/test/acceptance/fixtures/examples/tikz_feynman/options.json new file mode 100644 index 00000000..96a05433 --- /dev/null +++ b/test/acceptance/fixtures/examples/tikz_feynman/options.json @@ -0,0 +1,3 @@ +{ + "compiler": "lualatex" +} diff --git a/test/acceptance/fixtures/examples/tikz_feynman/output.pdf b/test/acceptance/fixtures/examples/tikz_feynman/output.pdf new file mode 100644 index 0000000000000000000000000000000000000000..367b0d0cae0da7e68989354c333dfc5323f1149d GIT binary patch literal 34780 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 literal 0 HcmV?d00001 From 3cffb61c7431797dde58b258afcd830ffe64233f Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 7 Feb 2017 08:49:19 +0000 Subject: [PATCH 185/709] add luatex85 package to tikz feynman test --- test/acceptance/fixtures/examples/tikz_feynman/main.tex | 1 + 1 file changed, 1 insertion(+) diff --git a/test/acceptance/fixtures/examples/tikz_feynman/main.tex b/test/acceptance/fixtures/examples/tikz_feynman/main.tex index c7520685..8e8860cd 100644 --- a/test/acceptance/fixtures/examples/tikz_feynman/main.tex +++ b/test/acceptance/fixtures/examples/tikz_feynman/main.tex @@ -1,3 +1,4 @@ +\usepackage{luatex85} \documentclass[tikz]{standalone} \usepackage[compat=1.1.0]{tikz-feynman} From 6cb5926c2172b3486d9e700410a338cc8fb2712e Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 7 Feb 2017 08:59:45 +0000 Subject: [PATCH 186/709] fix lualatex require --- test/acceptance/fixtures/examples/tikz_feynman/main.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/fixtures/examples/tikz_feynman/main.tex b/test/acceptance/fixtures/examples/tikz_feynman/main.tex index 8e8860cd..6071bc26 100644 --- a/test/acceptance/fixtures/examples/tikz_feynman/main.tex +++ b/test/acceptance/fixtures/examples/tikz_feynman/main.tex @@ -1,4 +1,4 @@ -\usepackage{luatex85} +\RequirePackage{luatex85} \documentclass[tikz]{standalone} \usepackage[compat=1.1.0]{tikz-feynman} From 5b71b849ca0ce6fbc44ae685e86fe9151658cbac Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 7 Feb 2017 10:00:41 +0000 Subject: [PATCH 187/709] added fontawesome acceptance test --- .../fixtures/examples/fontawesome/main.tex | 12 ++++++++++++ .../fixtures/examples/fontawesome/output.pdf | Bin 0 -> 26833 bytes 2 files changed, 12 insertions(+) create mode 100644 test/acceptance/fixtures/examples/fontawesome/main.tex create mode 100644 test/acceptance/fixtures/examples/fontawesome/output.pdf diff --git a/test/acceptance/fixtures/examples/fontawesome/main.tex b/test/acceptance/fixtures/examples/fontawesome/main.tex new file mode 100644 index 00000000..42bfa8e5 --- /dev/null +++ b/test/acceptance/fixtures/examples/fontawesome/main.tex @@ -0,0 +1,12 @@ +\documentclass{article} +\usepackage{fontawesome} + +\begin{document} +Cloud \faCloud + +Cog \faCog + +Database \faDatabase + +Leaf \faLeaf +\end{document} diff --git a/test/acceptance/fixtures/examples/fontawesome/output.pdf b/test/acceptance/fixtures/examples/fontawesome/output.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4032f42b566542cedc5733c1e446ceed628b0d16 GIT binary patch literal 26833 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≫Y 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 literal 0 HcmV?d00001 From 2898a82de89b64bbf4ea5711a8de34a389d491df Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 7 Feb 2017 11:51:21 +0000 Subject: [PATCH 188/709] update acceptance test output for fontawesome --- .../fixtures/examples/fontawesome/output.pdf | Bin 26833 -> 30768 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/fontawesome/output.pdf b/test/acceptance/fixtures/examples/fontawesome/output.pdf index 4032f42b566542cedc5733c1e446ceed628b0d16..327d317b29bc22807b26c78d64c1bb933f4a55b4 100644 GIT binary patch delta 30347 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 delta 26343 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 From 6b7e33bbc625c538035eac5caf246234738daa0a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 9 Feb 2017 14:16:53 +0000 Subject: [PATCH 189/709] show debug info for acceptance tests --- test/acceptance/scripts/full-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/scripts/full-test.sh b/test/acceptance/scripts/full-test.sh index 4b164532..cdbfa972 100755 --- a/test/acceptance/scripts/full-test.sh +++ b/test/acceptance/scripts/full-test.sh @@ -1,4 +1,4 @@ -#! /usr/bin/env bash +#! /usr/bin/env bash -x export SHARELATEX_CONFIG=`pwd`/test/acceptance/scripts/settings.test.coffee From fde8149579559a3f69281dd0aeeb1fc19f3ff0d8 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 9 Feb 2017 15:38:25 +0000 Subject: [PATCH 190/709] fix #! in test script --- test/acceptance/scripts/full-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/scripts/full-test.sh b/test/acceptance/scripts/full-test.sh index cdbfa972..0295eb5a 100755 --- a/test/acceptance/scripts/full-test.sh +++ b/test/acceptance/scripts/full-test.sh @@ -1,4 +1,4 @@ -#! /usr/bin/env bash -x +#!/bin/bash -x export SHARELATEX_CONFIG=`pwd`/test/acceptance/scripts/settings.test.coffee From 071b2269b3feec5e9c5bbad0188cba9eee56693e Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 13 Feb 2017 13:42:44 +0000 Subject: [PATCH 191/709] update acceptance tests for reversion to dvipdf --- .../fixtures/examples/feynmp/output.pdf | Bin 3668 -> 6268 bytes .../examples/latex_compiler/output.pdf | Bin 24239 -> 26325 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/feynmp/output.pdf b/test/acceptance/fixtures/examples/feynmp/output.pdf index 9428ca938518bd1cad467bb61106db1084b6ff9f..66268b2b7eac844487745c89f4e90e121a591753 100644 GIT binary patch literal 6268 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 literal 3668 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 diff --git a/test/acceptance/fixtures/examples/latex_compiler/output.pdf b/test/acceptance/fixtures/examples/latex_compiler/output.pdf index b87319f7d40b8c57b3e3153253f0cc6533375cf4..78e2440fa99c3d313ef91071d8d560984704ba5a 100644 GIT binary patch literal 26325 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%0A1){xm;=h{pf{6YSX+ilw{kV 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 delta 23066 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 KL|#Jy^nU;ZbbufL From 599977c3e0ce6d91a4e97e15e83b6ca2d8bb3e00 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 20 Feb 2017 15:16:52 +0000 Subject: [PATCH 192/709] if host has 1 cpu (staging) then set availableWorkingCpus to 1 --- app.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index d13798c4..ed3349b1 100644 --- a/app.coffee +++ b/app.coffee @@ -176,7 +176,12 @@ server = net.createServer (socket) -> socket.destroy() currentLoad = os.loadavg()[0] - availableWorkingCpus = os.cpus().length - 1 + + if os.cpus().length == 1 + availableWorkingCpus = 1 + else + availableWorkingCpus = os.cpus().length - 1 + freeLoad = availableWorkingCpus - currentLoad freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) if freeLoadPercentage <= 0 From bc1b8f4b2f7126f0810f6dbf9479c512fb4255a9 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 20 Feb 2017 15:19:04 +0000 Subject: [PATCH 193/709] Update app.coffee --- app.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index ed3349b1..b5e52206 100644 --- a/app.coffee +++ b/app.coffee @@ -176,7 +176,8 @@ server = net.createServer (socket) -> socket.destroy() currentLoad = os.loadavg()[0] - + + # On staging there may be 1 cpu on host, don't want to set availableWorkingCpus to 0 in that instance if os.cpus().length == 1 availableWorkingCpus = 1 else From a50582fd7ce853c2a6d00aa161b986c853c1aa28 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 21 Feb 2017 09:33:03 +0000 Subject: [PATCH 194/709] add fontawesome acceptance test for xelatex --- .../examples/fontawesome_xelatex/main.tex | 12 ++++++++++++ .../examples/fontawesome_xelatex/options.json | 3 +++ .../examples/fontawesome_xelatex/output.pdf | Bin 0 -> 30768 bytes 3 files changed, 15 insertions(+) create mode 100644 test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex create mode 100644 test/acceptance/fixtures/examples/fontawesome_xelatex/options.json create mode 100644 test/acceptance/fixtures/examples/fontawesome_xelatex/output.pdf diff --git a/test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex b/test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex new file mode 100644 index 00000000..42bfa8e5 --- /dev/null +++ b/test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex @@ -0,0 +1,12 @@ +\documentclass{article} +\usepackage{fontawesome} + +\begin{document} +Cloud \faCloud + +Cog \faCog + +Database \faDatabase + +Leaf \faLeaf +\end{document} diff --git a/test/acceptance/fixtures/examples/fontawesome_xelatex/options.json b/test/acceptance/fixtures/examples/fontawesome_xelatex/options.json new file mode 100644 index 00000000..a2e0c098 --- /dev/null +++ b/test/acceptance/fixtures/examples/fontawesome_xelatex/options.json @@ -0,0 +1,3 @@ +{ + "compiler": "xelatex" +} diff --git a/test/acceptance/fixtures/examples/fontawesome_xelatex/output.pdf b/test/acceptance/fixtures/examples/fontawesome_xelatex/output.pdf new file mode 100644 index 0000000000000000000000000000000000000000..327d317b29bc22807b26c78d64c1bb933f4a55b4 GIT binary patch literal 30768 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 Ds<j%{ literal 0 HcmV?d00001 From 29594fd0f7be23bf3c65648c82bc17ad93acd702 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 21 Feb 2017 09:34:04 +0000 Subject: [PATCH 195/709] fix acceptance test config file for latex prefix latex command prefix was in wrong scope --- test/acceptance/scripts/settings.test.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.coffee index a1f51ddb..dd6574d4 100644 --- a/test/acceptance/scripts/settings.test.coffee +++ b/test/acceptance/scripts/settings.test.coffee @@ -21,6 +21,7 @@ module.exports = #strace: true #archive_logs: true commandRunner: "docker-runner-sharelatex" + latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux docker: image: "texlive-full:2016.1-opt" env: @@ -29,7 +30,6 @@ module.exports = modem: socketPath: false user: "111" - latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux internal: clsi: From d4d3048719522a7e7c6043c4a816ee05d8cfb8c5 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 23 Feb 2017 11:09:18 +0000 Subject: [PATCH 196/709] remove tcp code, moved to agent load balancer --- app.coffee | 33 --------------------------------- config/settings.defaults.coffee | 1 - 2 files changed, 34 deletions(-) diff --git a/app.coffee b/app.coffee index b5e52206..c6498426 100644 --- a/app.coffee +++ b/app.coffee @@ -162,36 +162,3 @@ setInterval () -> ProjectPersistenceManager.clearExpiredProjects() , tenMinutes = 10 * 60 * 1000 - - -net = require('net') -os = require('os') - -server = net.createServer (socket) -> - socket.on "error", (err)-> - if err.code == "ECONNRESET" - # this always comes up, we don't know why - return - logger.err err:err, "error with socket on load check" - socket.destroy() - - currentLoad = os.loadavg()[0] - - # On staging there may be 1 cpu on host, don't want to set availableWorkingCpus to 0 in that instance - if os.cpus().length == 1 - availableWorkingCpus = 1 - else - availableWorkingCpus = os.cpus().length - 1 - - freeLoad = availableWorkingCpus - currentLoad - freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) - if freeLoadPercentage <= 0 - freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects to different servers - socket.write("up, #{freeLoadPercentage}%\n", "ASCII") - socket.end() - -server.listen load_port = (Settings.internal?.clsi?.load_port or 3044), -> - logger.info "tcp load endpoint listening on port #{load_port}" - # telnet 127.0.0.1 3044 - - diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index f1f7492a..cb7e6be7 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -35,7 +35,6 @@ module.exports = internal: clsi: port: 3013 - load_port: 3044 host: "localhost" From 57a5cfa9cb1146115ddfbd624fe5fd69a196c93b Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 2 Mar 2017 16:43:35 +0000 Subject: [PATCH 197/709] allow latexmk to pass through options this avoids problems in the latest version of latexmk where the $pdflatex variable has been replaced by $xelatex and $lualatex when running with -xelatex or -lualatex --- app/coffee/LatexRunner.coffee | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index e743cf01..efd89dfc 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -62,31 +62,32 @@ module.exports = LatexRunner = else CommandRunner.kill ProcessTable[id], callback - _latexmkBaseCommand: (Settings?.clsi?.latexmkCommandPrefix || []).concat( - ["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", + "-synctex=1","-interaction=batchmode" + ]) _pdflatexCommand: (mainFile) -> LatexRunner._latexmkBaseCommand.concat [ - "-pdf", "-e", "$pdflatex='pdflatex -synctex=1 -interaction=batchmode %O %S'", + "-pdf", Path.join("$COMPILE_DIR", mainFile) ] _latexCommand: (mainFile) -> LatexRunner._latexmkBaseCommand.concat [ - "-pdfdvi", "-e", "$latex='latex -synctex=1 -interaction=batchmode %O %S'", + "-pdfdvi", Path.join("$COMPILE_DIR", mainFile) ] _xelatexCommand: (mainFile) -> LatexRunner._latexmkBaseCommand.concat [ - "-xelatex", "-e", "$pdflatex='xelatex -synctex=1 -interaction=batchmode %O %S'", + "-xelatex", Path.join("$COMPILE_DIR", mainFile) ] _lualatexCommand: (mainFile) -> LatexRunner._latexmkBaseCommand.concat [ - "-pdf", "-e", "$pdflatex='lualatex -synctex=1 -interaction=batchmode %O %S'", + "-lualatex", Path.join("$COMPILE_DIR", mainFile) ] From a0969ec839e226b95c601b5f8832f8f2d20b5d6d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Mon, 6 Mar 2017 14:43:14 +0000 Subject: [PATCH 198/709] Don't compile acceptance test files during test run --- test/acceptance/scripts/full-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/scripts/full-test.sh b/test/acceptance/scripts/full-test.sh index 0295eb5a..a465bdde 100755 --- a/test/acceptance/scripts/full-test.sh +++ b/test/acceptance/scripts/full-test.sh @@ -11,7 +11,7 @@ echo ">> Server started" sleep 5 echo ">> Running acceptance tests..." -grunt --no-color test:acceptance +grunt --no-color mochaTest:acceptance _test_exit_code=$? echo ">> Killing server" From 03d1936fde033a78ff40348e1b96eb280a4b4904 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Mon, 6 Mar 2017 14:56:32 +0000 Subject: [PATCH 199/709] Upgrade logger --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65367df1..73bb08af 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.4.0", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", "sequelize": "^2.1.3", From efe5e22b4ca2fd436da1268bcd0e53e95490b14a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 8 Mar 2017 11:25:09 +0000 Subject: [PATCH 200/709] include otf extension in fontawesome test --- .../acceptance/fixtures/examples/fontawesome_xelatex/main.tex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex b/test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex index 42bfa8e5..5158b672 100644 --- a/test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex +++ b/test/acceptance/fixtures/examples/fontawesome_xelatex/main.tex @@ -1,4 +1,8 @@ \documentclass{article} +\usepackage{fontspec} +\defaultfontfeatures{Extension = .otf} % this is needed because + % fontawesome package loads by + % font name only \usepackage{fontawesome} \begin{document} From 7a7c2ee9920e612cdf1485cb3efaaf62d5447c22 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 8 Mar 2017 11:48:36 +0000 Subject: [PATCH 201/709] improve debugging of failed acceptance tests use the example name in the output filename --- test/acceptance/coffee/ExampleDocumentTests.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 6c9e96bf..43dbff3c 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -16,11 +16,13 @@ convertToPng = (pdfPath, pngPath, callback = (error) ->) -> callback() compare = (originalPath, generatedPath, callback = (error, same) ->) -> - proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{fixturePath("tmp/diff.png")}" + diff_file = "#{fixturePath(generatedPath)}-diff.png" + proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{diff_file}" stderr = "" proc.stderr.on "data", (chunk) -> stderr += chunk proc.on "exit", () -> if stderr.trim() == "0 (0)" + fs.unlink diff_file # remove output diff if test matches expected image callback null, true else console.log "compare result", stderr @@ -68,7 +70,7 @@ describe "Example Documents", -> do (example_dir) -> describe example_dir, -> before -> - @project_id = Client.randomId() + @project_id = Client.randomId() + "_" + example_dir it "should generate the correct pdf", (done) -> Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => From f059948e27499b8ad6882eca4d1bfa815c32e244 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 8 Mar 2017 11:49:21 +0000 Subject: [PATCH 202/709] update xelatex acceptance test pdf --- .../examples/fontawesome_xelatex/output.pdf | Bin 30768 -> 6613 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/fontawesome_xelatex/output.pdf b/test/acceptance/fixtures/examples/fontawesome_xelatex/output.pdf index 327d317b29bc22807b26c78d64c1bb933f4a55b4..b5a12e39a6d5d056516d97b716bfbbdd4d7362cd 100644 GIT binary patch literal 6613 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 X`zS2V2a9$f1A|~>{QRo=YGnTf$lwzT literal 30768 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 Ds<j%{ From 5af137f60b38899ea6c82256815ed91e4511d0cc Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 20 Mar 2017 10:03:48 +0000 Subject: [PATCH 203/709] additional check for valid rootResource --- app/coffee/RequestParser.coffee | 9 ++++++++- test/unit/coffee/RequestParserTests.coffee | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 90bc739f..8fc4ecf3 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -44,7 +44,7 @@ module.exports = RequestParser = type: "string" originalRootResourcePath = rootResourcePath sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath) - response.rootResourcePath = sanitizedRootResourcePath + response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath) for resource in response.resources if resource.path == originalRootResourcePath @@ -92,3 +92,10 @@ module.exports = RequestParser = _sanitizePath: (path) -> # See http://php.net/manual/en/function.escapeshellcmd.php path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, "") + + _checkPath: (path) -> + # check that the request does not use a relative path + for dir in path.split('/') + if dir == '..' + throw "relative path in root resource" + return path diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index 4cf61198..1cd931bc 100644 --- a/test/unit/coffee/RequestParserTests.coffee +++ b/test/unit/coffee/RequestParserTests.coffee @@ -223,4 +223,22 @@ describe "RequestParser", -> it "should also escape the resource path", -> @data.resources[0].path.should.equal @goodPath + describe "with a root resource path that has a relative path", -> + beforeEach -> + @validRequest.compile.rootResourcePath = "foo/../../bar.tex" + @RequestParser.parse @validRequest, @callback + @data = @callback.args[0][1] + + it "should return an error", -> + @callback.calledWith("relative path in root resource") + .should.equal true + describe "with a root resource path that has unescaped + relative path", -> + beforeEach -> + @validRequest.compile.rootResourcePath = "foo/#../bar.tex" + @RequestParser.parse @validRequest, @callback + @data = @callback.args[0][1] + + it "should return an error", -> + @callback.calledWith("relative path in root resource") + .should.equal true From 8803762081e2beccdc274af41703497d1674722e Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 16 Mar 2017 16:55:53 +0000 Subject: [PATCH 204/709] support for tikz externalize make copy of main file as output.tex for tikz externalize --- app/coffee/CompileManager.coffee | 10 ++++- app/coffee/ResourceWriter.coffee | 2 + app/coffee/TikzManager.coffee | 37 ++++++++++++++++ test/unit/coffee/TikzManager.coffee | 65 +++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 app/coffee/TikzManager.coffee create mode 100644 test/unit/coffee/TikzManager.coffee diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 0a1ea9d1..e4cb452f 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -8,6 +8,7 @@ logger = require "logger-sharelatex" Metrics = require "./Metrics" child_process = require "child_process" DraftModeManager = require "./DraftModeManager" +TikzManager = require "./TikzManager" fs = require("fs") fse = require "fs-extra" os = require("os") @@ -42,6 +43,12 @@ module.exports = CompileManager = else callback() + createTikzFileIfRequired = (callback) -> + if TikzManager.needsOutputFile(request.rootResourcePath, request.resources) + TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback + else + callback() + # set up environment variables for chktex env = {} # only run chktex on LaTeX files (not knitr .Rtex files or any others) @@ -54,7 +61,8 @@ module.exports = CompileManager = if request.check is 'validate' env['CHKTEX_VALIDATE'] = 1 - injectDraftModeIfRequired (error) -> + # apply a series of file modifications/creations for draft mode and tikz + async.series [injectDraftModeIfRequired, createTikzFileIfRequired], (error) -> return callback(error) if error? timer = new Metrics.Timer("run-compile") # find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 16000cbb..a7da5886 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -50,6 +50,8 @@ module.exports = ResourceWriter = should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" should_delete = true + if path == "output.tex" # created by TikzManager if present in output files + should_delete = true if should_delete jobs.push (callback) -> ResourceWriter._deleteFileIfNotDirectory Path.join(basePath, path), callback diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee new file mode 100644 index 00000000..a7f29e9c --- /dev/null +++ b/app/coffee/TikzManager.coffee @@ -0,0 +1,37 @@ +fs = require "fs" +Path = require "path" +logger = require "logger-sharelatex" + +# for \tikzexternalize to work the main file needs to match the +# jobname. Since we set the -jobname to output, we have to create a +# copy of the main file as 'output.tex'. + +module.exports = TikzManager = + needsOutputFile: (rootResourcePath, resources) -> + # if there's already an output.tex file, we don't want to touch it + for resource in resources + if resource.path is "output.tex" + return false + # if there's no output.tex, see if we are using tikz/pgf in the main file + for resource in resources + if resource.path is rootResourcePath + return TikzManager._includesTikz (resource) + # otherwise false + return false + + _includesTikz: (resource) -> + # check if we are loading tikz or pgf + content = resource.content.slice(0,4096) + if content.indexOf("\\usepackage{tikz") >= 0 + return true + else if content.indexOf("\\usepackage{pgf") >= 0 + return true + else + return false + + injectOutputFile: (compileDir, mainFile, callback = (error) ->) -> + fs.readFile Path.join(compileDir, mainFile), "utf8", (error, content) -> + return callback(error) if error? + logger.log compileDir: compileDir, mainFile: mainFile, "copied file to ouput.tex for tikz" + # use wx flag to ensure that output file does not already exist + fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.coffee new file mode 100644 index 00000000..859c392e --- /dev/null +++ b/test/unit/coffee/TikzManager.coffee @@ -0,0 +1,65 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/TikzManager' + +describe 'TikzManager', -> + beforeEach -> + @TikzManager = SandboxedModule.require modulePath, requires: + "fs": @fs = {} + "logger-sharelatex": @logger = {log: () ->} + + describe "needsOutputFile", -> + it "should return true if there is a usepackage{tikz}", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex' }, + { path: 'main.tex', content:'foo \\usepackage{tikz}' } + ]).should.equal true + + it "should return true if there is a usepackage{pgf}", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex'}, + { path: 'main.tex', content:'foo \\usepackage{pgf}' } + ]).should.equal true + + it "should return false if there is no usepackage{tikz} or {pgf}", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex' }, + { path: 'main.tex', content:'foo \\usepackage{bar}' } + ]).should.equal false + + it "should return false if there is already an output.tex file", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex' }, + { path: 'main.tex' }, + { path: 'output.tex' } + ]).should.equal false + + describe "injectOutputFile", -> + beforeEach -> + @rootDir = "/mock" + @filename = "filename.tex" + @callback = sinon.stub() + @content = ''' + \\documentclass{article} + \\usepackage{tikz} + \\begin{document} + Hello world + \\end{document} + ''' + @fs.readFile = sinon.stub().callsArgWith(2, null, @content) + @fs.writeFile = sinon.stub().callsArg(3) + @TikzManager.injectOutputFile @rootDir, @filename, @callback + + it "should read the file", -> + @fs.readFile + .calledWith("#{@rootDir}/#{@filename}", "utf8") + .should.equal true + + it "should write out the same file as output.tex", -> + @fs.writeFile + .calledWith("#{@rootDir}/output.tex", @content, {flag: 'wx'}) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true From 021d84881935461db707c6a77c8482449e297cfa Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 21 Mar 2017 11:29:37 +0000 Subject: [PATCH 205/709] create separate function for path checking --- app/coffee/ResourceWriter.coffee | 31 +++++++++++---------- app/coffee/TikzManager.coffee | 11 +++++--- test/unit/coffee/ResourceWriterTests.coffee | 17 +++++++++-- test/unit/coffee/TikzManager.coffee | 6 ++++ 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index a7da5886..8c3245f3 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -75,19 +75,22 @@ module.exports = ResourceWriter = callback() _writeResourceToDisk: (project_id, resource, basePath, callback = (error) ->) -> - path = Path.normalize(Path.join(basePath, resource.path)) - if (path.slice(0, basePath.length) != basePath) - return callback new Error("resource path is outside root directory") - - mkdirp Path.dirname(path), (error) -> + ResourceWriter.checkPath basePath, resource.path, (error, path) -> return callback(error) if error? - # TODO: Don't overwrite file if it hasn't been modified - if resource.url? - UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)-> - if err? - logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources" - callback() #try and continue compiling even if http resource can not be downloaded at this time - else - fs.writeFile path, resource.content, callback + mkdirp Path.dirname(path), (error) -> + return callback(error) if error? + # TODO: Don't overwrite file if it hasn't been modified + if resource.url? + UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)-> + if err? + logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources" + callback() #try and continue compiling even if http resource can not be downloaded at this time + else + fs.writeFile path, resource.content, callback - + checkPath: (basePath, resourcePath, callback) -> + path = Path.normalize(Path.join(basePath, resourcePath)) + if (path.slice(0, basePath.length) != basePath) + return callback new Error("resource path is outside root directory") + else + return callback(null, path) diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index a7f29e9c..7766bc12 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -1,5 +1,6 @@ fs = require "fs" Path = require "path" +ResourceWriter = require "./ResourceWriter" logger = require "logger-sharelatex" # for \tikzexternalize to work the main file needs to match the @@ -30,8 +31,10 @@ module.exports = TikzManager = return false injectOutputFile: (compileDir, mainFile, callback = (error) ->) -> - fs.readFile Path.join(compileDir, mainFile), "utf8", (error, content) -> + ResourceWriter.checkPath compileDir, mainFile, (error, path) -> return callback(error) if error? - logger.log compileDir: compileDir, mainFile: mainFile, "copied file to ouput.tex for tikz" - # use wx flag to ensure that output file does not already exist - fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback + fs.readFile path, "utf8", (error, content) -> + return callback(error) if error? + logger.log compileDir: compileDir, mainFile: mainFile, "copied file to ouput.tex for tikz" + # use wx flag to ensure that output file does not already exist + fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index c1e72a87..c3c25cc2 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -157,6 +157,19 @@ describe "ResourceWriter", -> .calledWith(new Error("resource path is outside root directory")) .should.equal true - + describe "checkPath", -> + describe "with a valid path", -> + beforeEach -> + @ResourceWriter.checkPath("foo", "bar", @callback) - + it "should return the joined path", -> + @callback.calledWith(null, "foo/bar") + .should.equal true + + describe "with an invalid path", -> + beforeEach -> + @ResourceWriter.checkPath("foo", "baz/../../bar", @callback) + + it "should return an error", -> + @callback.calledWith(new Error("resource path is outside root directory")) + .should.equal true diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.coffee index 859c392e..4c99daac 100644 --- a/test/unit/coffee/TikzManager.coffee +++ b/test/unit/coffee/TikzManager.coffee @@ -6,6 +6,7 @@ modulePath = require('path').join __dirname, '../../../app/js/TikzManager' describe 'TikzManager', -> beforeEach -> @TikzManager = SandboxedModule.require modulePath, requires: + "./ResourceWriter": @ResourceWriter = {} "fs": @fs = {} "logger-sharelatex": @logger = {log: () ->} @@ -49,8 +50,13 @@ describe 'TikzManager', -> ''' @fs.readFile = sinon.stub().callsArgWith(2, null, @content) @fs.writeFile = sinon.stub().callsArg(3) + @ResourceWriter.checkPath = sinon.stub().callsArgWith(2, null, "#{@rootDir}/#{@filename}") @TikzManager.injectOutputFile @rootDir, @filename, @callback + it "sould check the path", -> + @ResourceWriter.checkPath.calledWith(@rootDir, @filename) + .should.equal true + it "should read the file", -> @fs.readFile .calledWith("#{@rootDir}/#{@filename}", "utf8") From 750576d1b04be44c5cbf757ed4c5648d050c44cc Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 21 Mar 2017 11:30:32 +0000 Subject: [PATCH 206/709] fix path match --- app/coffee/ResourceWriter.coffee | 2 +- test/unit/coffee/ResourceWriterTests.coffee | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 8c3245f3..2bf65980 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -90,7 +90,7 @@ module.exports = ResourceWriter = checkPath: (basePath, resourcePath, callback) -> path = Path.normalize(Path.join(basePath, resourcePath)) - if (path.slice(0, basePath.length) != basePath) + if (path.slice(0, basePath.length + 1) != basePath + "/") return callback new Error("resource path is outside root directory") else return callback(null, path) diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index c3c25cc2..96140c98 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -173,3 +173,11 @@ describe "ResourceWriter", -> it "should return an error", -> @callback.calledWith(new Error("resource path is outside root directory")) .should.equal true + + describe "with another invalid path matching on a prefix", -> + beforeEach -> + @ResourceWriter.checkPath("foo", "../foobar/baz", @callback) + + it "should return an error", -> + @callback.calledWith(new Error("resource path is outside root directory")) + .should.equal true From 7ccc9500edbd8a1c46b3aadbbd4f17cd0209d1e1 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 21 Mar 2017 11:36:08 +0000 Subject: [PATCH 207/709] check for \tikzexternalize directly instead of \usepackage{tikz} and \usepackage{pgf} --- app/coffee/TikzManager.coffee | 8 +++----- test/unit/coffee/TikzManager.coffee | 17 ++++++----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index 7766bc12..cfa13321 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -21,11 +21,9 @@ module.exports = TikzManager = return false _includesTikz: (resource) -> - # check if we are loading tikz or pgf - content = resource.content.slice(0,4096) - if content.indexOf("\\usepackage{tikz") >= 0 - return true - else if content.indexOf("\\usepackage{pgf") >= 0 + # check if we are using tikz externalize + content = resource.content.slice(0,65536) + if content.indexOf("\\tikzexternalize") >= 0 return true else return false diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.coffee index 4c99daac..8174b4a2 100644 --- a/test/unit/coffee/TikzManager.coffee +++ b/test/unit/coffee/TikzManager.coffee @@ -11,28 +11,22 @@ describe 'TikzManager', -> "logger-sharelatex": @logger = {log: () ->} describe "needsOutputFile", -> - it "should return true if there is a usepackage{tikz}", -> + it "should return true if there is a \\tikzexternalize", -> @TikzManager.needsOutputFile("main.tex", [ { path: 'foo.tex' }, - { path: 'main.tex', content:'foo \\usepackage{tikz}' } + { path: 'main.tex', content:'foo \\usepackage{tikz} \\tikzexternalize' } ]).should.equal true - it "should return true if there is a usepackage{pgf}", -> - @TikzManager.needsOutputFile("main.tex", [ - { path: 'foo.tex'}, - { path: 'main.tex', content:'foo \\usepackage{pgf}' } - ]).should.equal true - - it "should return false if there is no usepackage{tikz} or {pgf}", -> + it "should return false if there is no \\tikzexternalize", -> @TikzManager.needsOutputFile("main.tex", [ { path: 'foo.tex' }, - { path: 'main.tex', content:'foo \\usepackage{bar}' } + { path: 'main.tex', content:'foo \\usepackage{tikz}' } ]).should.equal false it "should return false if there is already an output.tex file", -> @TikzManager.needsOutputFile("main.tex", [ { path: 'foo.tex' }, - { path: 'main.tex' }, + { path: 'main.tex', content:'foo \\usepackage{tikz} \\tikzexternalize' }, { path: 'output.tex' } ]).should.equal false @@ -44,6 +38,7 @@ describe 'TikzManager', -> @content = ''' \\documentclass{article} \\usepackage{tikz} + \\tikzexternalize \\begin{document} Hello world \\end{document} From 834ad57312042bc7448fed8bb62e02c7cd792bf5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Mon, 27 Mar 2017 14:47:48 +0100 Subject: [PATCH 208/709] Add a .nvmrc file --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..994fe990 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +0.10.22 \ No newline at end of file From eb1364f249c25ca8fad11fd002b8246bae00129d Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 4 Apr 2017 16:50:06 +0100 Subject: [PATCH 209/709] check if file is optimised before running qpdf --- app/coffee/OutputFileOptimiser.coffee | 17 ++- .../coffee/OutputFileOptimiserTests.coffee | 103 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 test/unit/coffee/OutputFileOptimiserTests.coffee diff --git a/app/coffee/OutputFileOptimiser.coffee b/app/coffee/OutputFileOptimiser.coffee index eb5266e1..b702f36f 100644 --- a/app/coffee/OutputFileOptimiser.coffee +++ b/app/coffee/OutputFileOptimiser.coffee @@ -11,10 +11,25 @@ module.exports = OutputFileOptimiser = # check output file (src) and see if we can optimise it, storing # the result in the build directory (dst) if src.match(/\/output\.pdf$/) - OutputFileOptimiser.optimisePDF src, dst, callback + OutputFileOptimiser.checkIfPDFIsOptimised src, (err, isOptimised) -> + return callback(null) if err? or isOptimised + OutputFileOptimiser.optimisePDF src, dst, callback else callback (null) + checkIfPDFIsOptimised: (file, callback) -> + SIZE = 16*1024 # check the header of the pdf + result = new Buffer(SIZE) + result.fill(0) # prevent leakage of uninitialised buffer + fs.open file, "r", (err, fd) -> + return callback(err) if err? + fs.read fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) -> + fs.close fd, (errClose) -> + return callback(errRead) if errRead? + return callback(errClose) if errReadClose? + isOptimised = buffer.toString('ascii').indexOf("/Linearized 1") >= 0 + callback(null, isOptimised) + optimisePDF: (src, dst, callback = (error) ->) -> tmpOutput = dst + '.opt' args = ["--linearize", src, tmpOutput] diff --git a/test/unit/coffee/OutputFileOptimiserTests.coffee b/test/unit/coffee/OutputFileOptimiserTests.coffee new file mode 100644 index 00000000..29887155 --- /dev/null +++ b/test/unit/coffee/OutputFileOptimiserTests.coffee @@ -0,0 +1,103 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/OutputFileOptimiser' +path = require "path" +expect = require("chai").expect +EventEmitter = require("events").EventEmitter + +describe "OutputFileOptimiser", -> + beforeEach -> + @OutputFileOptimiser = SandboxedModule.require modulePath, requires: + "fs": @fs = {} + "path": @Path = {} + "child_process": spawn: @spawn = sinon.stub() + "logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() } + "./Metrics" : {} + @directory = "/test/dir" + @callback = sinon.stub() + + describe "optimiseFile", -> + beforeEach -> + @src = "./output.pdf" + @dst = "./output.pdf" + + describe "when the file is not a pdf file", -> + beforeEach (done)-> + @src = "./output.log" + @OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false) + @OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null) + @OutputFileOptimiser.optimiseFile @src, @dst, done + + it "should not check if the file is optimised", -> + @OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(@src).should.equal false + + it "should not optimise the file", -> + @OutputFileOptimiser.optimisePDF.calledWith(@src, @dst).should.equal false + + describe "when the pdf file is not optimised", -> + beforeEach (done) -> + @OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false) + @OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null) + @OutputFileOptimiser.optimiseFile @src, @dst, done + + it "should check if the pdf is optimised", -> + @OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(@src).should.equal true + + it "should optimise the pdf", -> + @OutputFileOptimiser.optimisePDF.calledWith(@src, @dst).should.equal true + + describe "when the pdf file is optimised", -> + beforeEach (done) -> + @OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, true) + @OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null) + @OutputFileOptimiser.optimiseFile @src, @dst, done + + it "should check if the pdf is optimised", -> + @OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(@src).should.equal true + + it "should not optimise the pdf", -> + @OutputFileOptimiser.optimisePDF.calledWith(@src, @dst).should.equal false + + describe "checkIfPDFISOptimised", -> + beforeEach () -> + @callback = sinon.stub() + @fd = 1234 + @fs.open = sinon.stub().yields(null, @fd) + @fs.read = sinon.stub().withArgs(@fd).yields(null, 100, new Buffer("hello /Linearized 1")) + @fs.close = sinon.stub().withArgs(@fd).yields(null) + @OutputFileOptimiser.checkIfPDFIsOptimised @src, @callback + + describe "for a linearised file", -> + beforeEach () -> + @fs.read = sinon.stub().withArgs(@fd).yields(null, 100, new Buffer("hello /Linearized 1")) + @OutputFileOptimiser.checkIfPDFIsOptimised @src, @callback + + it "should open the file", -> + @fs.open.calledWith(@src, "r").should.equal true + + it "should read the header", -> + @fs.read.calledWith(@fd).should.equal true + + it "should close the file", -> + @fs.close.calledWith(@fd).should.equal true + + it "should call the callback with a true result", -> + @callback.calledWith(null, true).should.equal true + + describe "for an unlinearised file", -> + beforeEach () -> + @fs.read = sinon.stub().withArgs(@fd).yields(null, 100, new Buffer("hello not linearized 1")) + @OutputFileOptimiser.checkIfPDFIsOptimised @src, @callback + + it "should open the file", -> + @fs.open.calledWith(@src, "r").should.equal true + + it "should read the header", -> + @fs.read.calledWith(@fd).should.equal true + + it "should close the file", -> + @fs.close.calledWith(@fd).should.equal true + + it "should call the callback with a false result", -> + @callback.calledWith(null, false).should.equal true From e1b44beb3fcf30c0d58c4f577d2a3a29d3d547b9 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 7 Apr 2017 11:11:27 +0100 Subject: [PATCH 210/709] use pdfinfo on output to ensure pdfs are optimised needed to check that qpdf runs correctly inside the docker container --- .../coffee/ExampleDocumentTests.coffee | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 43dbff3c..a49f5d62 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -28,6 +28,18 @@ compare = (originalPath, generatedPath, callback = (error, same) ->) -> console.log "compare result", stderr callback null, false +checkPdfInfo = (pdfPath, callback = (error, output) ->) -> + proc = ChildProcess.exec "pdfinfo #{fixturePath(pdfPath)}" + stdout = "" + proc.stdout.on "data", (chunk) -> stdout += chunk + proc.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() + proc.on "exit", () -> + if stdout.match(/Optimized:\s+yes/) + callback null, true + else + console.log "pdfinfo result", stdout + callback null, false + compareMultiplePages = (project_id, callback = (error) ->) -> compareNext = (page_no, callback) -> path = "tmp/#{project_id}-source-#{page_no}.png" @@ -41,24 +53,30 @@ compareMultiplePages = (project_id, callback = (error) ->) -> compareNext page_no + 1, callback compareNext 0, callback +comparePdf = (project_id, example_dir, callback = (error) ->) -> + convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => + throw error if error? + convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => + throw error if error? + fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) => + if error? + compare "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) => + throw error if error? + same.should.equal true + callback() + else + compareMultiplePages project_id, (error) -> + throw error if error? + callback() + downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) -> writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf")) request.get(url).pipe(writeStream) writeStream.on "close", () => - convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => + checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) => throw error if error? - convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => - throw error if error? - fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) => - if error? - compare "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) => - throw error if error? - same.should.equal true - callback() - else - compareMultiplePages project_id, (error) -> - throw error if error? - callback() + optimised.should.equal true + comparePdf project_id, example_dir, callback Client.runServer(4242, fixturePath("examples")) From a1613eac5a713e95863d0c789bf0790446f0f4fb Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 10 Apr 2017 16:12:03 +0100 Subject: [PATCH 211/709] add setting to avoid optimisations outside docker --- app/coffee/OutputCacheManager.coffee | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 91e085ae..465b0431 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -172,8 +172,13 @@ module.exports = OutputCacheManager = logger.error err: err, src: src, dst: dst, "copy error for file in cache" callback(err) else - # call the optimiser for the file too - OutputFileOptimiser.optimiseFile src, dst, callback + if Settings.clsi?.optimiseInDocker + # don't run any optimisations on the pdf when they are done + # in the docker container + callback() + else + # call the optimiser for the file too + OutputFileOptimiser.optimiseFile src, dst, callback _checkIfShouldCopy: (src, callback = (err, shouldCopy) ->) -> return callback(null, !Path.basename(src).match(/^strace/)) From a98b2b80325a5bc51f6a63e18db3d4ec28937cd4 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 24 May 2017 09:41:26 +0100 Subject: [PATCH 212/709] don't report compile timeouts to sentry just log them instead --- app/coffee/CompileController.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 56237bf2..250f9b8e 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -20,12 +20,13 @@ module.exports = CompileController = 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 status = "timedout" + logger.log err: error, project_id: request.project_id, "timeout running compile" else status = "error" code = 500 + logger.error err: error, project_id: request.project_id, "error running compile" else status = "failure" for file in outputFiles From aafa691119936f2f9ec08d216b9f80a1c509c855 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 24 May 2017 10:03:53 +0100 Subject: [PATCH 213/709] check file exists before running synctex --- app/coffee/CompileManager.coffee | 22 ++++++++++++++------- test/unit/coffee/CompileManagerTests.coffee | 2 ++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index e4cb452f..7caa20c0 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -201,16 +201,24 @@ module.exports = CompileManager = logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) + _checkFileExists: (path, callback = (error) ->) -> + fs.stat path, (err, stats) -> + return callback(err) if err? + return callback(new Error("not a file")) if not stats.isFile() + callback() + _runSynctex: (args, callback = (error, stdout) ->) -> bin_path = Path.resolve(__dirname + "/../../bin/synctex") seconds = 1000 - 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) - callback(null, stdout) + outputFilePath = args[1] + CompileManager._checkFileExists outputFilePath, (err) -> + 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) + callback(null, stdout) _parseSynctexFromCodeOutput: (output) -> results = [] diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index e3f0705d..68629a1c 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -192,6 +192,7 @@ describe "CompileManager", -> describe "syncFromCode", -> beforeEach -> + @fs.stat = sinon.stub().callsArgWith(1, null,{isFile: ()->true}) @child_process.execFile.callsArgWith(3, null, @stdout = "NODE\t#{@page}\t#{@h}\t#{@v}\t#{@width}\t#{@height}\n", "") @CompileManager.syncFromCode @project_id, @user_id, @file_name, @line, @column, @callback @@ -216,6 +217,7 @@ describe "CompileManager", -> describe "syncFromPdf", -> beforeEach -> + @fs.stat = sinon.stub().callsArgWith(1, null,{isFile: ()->true}) @child_process.execFile.callsArgWith(3, null, @stdout = "NODE\t#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}\t#{@line}\t#{@column}\n", "") @CompileManager.syncFromPdf @project_id, @user_id, @page, @h, @v, @callback From c62f8b48540f7a23adda6feea9729df45eef45b5 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 24 May 2017 10:03:53 +0100 Subject: [PATCH 214/709] check directory exists and bail out on error --- app/coffee/CompileManager.coffee | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 7caa20c0..27aa394e 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -202,16 +202,25 @@ module.exports = CompileManager = callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) _checkFileExists: (path, callback = (error) ->) -> - fs.stat path, (err, stats) -> - return callback(err) if err? - return callback(new Error("not a file")) if not stats.isFile() - callback() + synctexDir = Path.dirname(path) + synctexFile = Path.join(synctexDir, "output.synctex.gz") + fs.stat synctexDir, (error, stats) -> + if error?.code is 'ENOENT' + return callback(new Error("called synctex with no output directory")) + return callback(error) if error? + fs.stat synctexFile, (error, stats) -> + if error?.code is 'ENOENT' + return callback(new Error("called synctex with no output file")) + return callback(error) if error? + return callback(new Error("not a file")) if not stats?.isFile() + callback() _runSynctex: (args, callback = (error, stdout) ->) -> bin_path = Path.resolve(__dirname + "/../../bin/synctex") seconds = 1000 outputFilePath = args[1] - CompileManager._checkFileExists outputFilePath, (err) -> + CompileManager._checkFileExists outputFilePath, (error) -> + return callback(error) if error? 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) -> From 2edc0156635e223309971c9815eb4aac51a18a63 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 15 Jun 2017 15:37:45 +0100 Subject: [PATCH 215/709] delete intermediate xdv files from xelatex --- app/coffee/ResourceWriter.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 2bf65980..e2a0e1f8 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -48,7 +48,7 @@ module.exports = ResourceWriter = should_delete = true if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false - if path == "output.pdf" or path == "output.dvi" or path == "output.log" + if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" should_delete = true if path == "output.tex" # created by TikzManager if present in output files should_delete = true From 8e2584bab47be534c5cb94bda9283e8f6c47ae14 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Tue, 20 Jun 2017 08:25:50 +0100 Subject: [PATCH 216/709] Mock out logger in tests --- test/unit/coffee/ResourceWriterTests.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 96140c98..fd1ae309 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -12,6 +12,7 @@ describe "ResourceWriter", -> "./UrlCache" : @UrlCache = {} "mkdirp" : @mkdirp = sinon.stub().callsArg(1) "./OutputFileFinder": @OutputFileFinder = {} + "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()} "./Metrics": @Metrics = Timer: class Timer done: sinon.stub() From aa1dd2bf05e6d8e57e1a7a1ddb186b12edf84a09 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Tue, 20 Jun 2017 09:18:15 +0100 Subject: [PATCH 217/709] Killing an already stopped project is not an error Log a warning instead and continue. --- app/coffee/LatexRunner.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index efd89dfc..6a5a4f6a 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -58,7 +58,8 @@ module.exports = LatexRunner = id = "#{project_id}" logger.log {id:id}, "killing running compile" if not ProcessTable[id]? - return callback new Error("no such project to kill") + logger.warn {id}, "no such project to kill" + return callback(null) else CommandRunner.kill ProcessTable[id], callback From a74f4ac1a68e355545d4bb7e8e39b0e45d20fe31 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Fri, 23 Jun 2017 14:46:40 +0100 Subject: [PATCH 218/709] Send a 404 if the project files have gone away when running synctex. This is semantically nicer than the 500 response which used to be produced in these circumstances. --- app.coffee | 9 +++++++-- app/coffee/CompileManager.coffee | 5 +++-- app/coffee/Errors.coffee | 10 ++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 app/coffee/Errors.coffee diff --git a/app.coffee b/app.coffee index c6498426..b99b2774 100644 --- a/app.coffee +++ b/app.coffee @@ -7,6 +7,7 @@ if Settings.sentry?.dsn? smokeTest = require "smoke-test-sharelatex" ContentTypeMapper = require "./app/js/ContentTypeMapper" +Errors = require './app/js/Errors' Path = require "path" fs = require "fs" @@ -152,8 +153,12 @@ app.get "/heapdump", (req, res)-> res.send filename app.use (error, req, res, next) -> - logger.error err: error, "server error" - res.sendStatus(error?.statusCode || 500) + if error instanceof Errors.NotFoundError + logger.warn {err: error, url: req.url}, "not found error" + return res.sendStatus(404) + else + logger.error {err: error, url: req.url}, "server error" + res.sendStatus(error?.statusCode || 500) app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) -> logger.info "CLSI starting up, listening on #{host}:#{port}" diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 27aa394e..ae40bf79 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -13,6 +13,7 @@ fs = require("fs") fse = require "fs-extra" os = require("os") async = require "async" +Errors = require './Errors' commandRunner = Settings.clsi?.commandRunner or "./CommandRunner" logger.info commandRunner:commandRunner, "selecting command runner for clsi" @@ -206,11 +207,11 @@ module.exports = CompileManager = synctexFile = Path.join(synctexDir, "output.synctex.gz") fs.stat synctexDir, (error, stats) -> if error?.code is 'ENOENT' - return callback(new Error("called synctex with no output directory")) + return callback(new Errors.NotFoundError("called synctex with no output directory")) return callback(error) if error? fs.stat synctexFile, (error, stats) -> if error?.code is 'ENOENT' - return callback(new Error("called synctex with no output file")) + return callback(new Errors.NotFoundError("called synctex with no output file")) return callback(error) if error? return callback(new Error("not a file")) if not stats?.isFile() callback() diff --git a/app/coffee/Errors.coffee b/app/coffee/Errors.coffee new file mode 100644 index 00000000..4abea0bb --- /dev/null +++ b/app/coffee/Errors.coffee @@ -0,0 +1,10 @@ +NotFoundError = (message) -> + error = new Error(message) + error.name = "NotFoundError" + error.__proto__ = NotFoundError.prototype + return error +NotFoundError.prototype.__proto__ = Error.prototype + + +module.exports = Errors = + NotFoundError: NotFoundError From 2e91f07014629658917ed73becf88755c74c6ea2 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 12 Jul 2017 16:58:48 +0100 Subject: [PATCH 219/709] update acceptance tests settings to 2017 image --- test/acceptance/scripts/settings.test.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.coffee index dd6574d4..cf84c6ba 100644 --- a/test/acceptance/scripts/settings.test.coffee +++ b/test/acceptance/scripts/settings.test.coffee @@ -23,9 +23,9 @@ module.exports = commandRunner: "docker-runner-sharelatex" latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux docker: - image: "texlive-full:2016.1-opt" + image: "texlive-full:2017.1-opt" env: - PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2016/bin/x86_64-linux/" + PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/" HOME: "/tmp" modem: socketPath: false From ea34a1a89d586d887eaf58d17cfb75149b5927ed Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 13 Jul 2017 13:15:51 +0100 Subject: [PATCH 220/709] update acceptance test images for texlive 2017 --- .../fixtures/examples/asymptote/output.pdf | Bin 122452 -> 123688 bytes .../fixtures/examples/knitr/output.pdf | Bin 43236 -> 44072 bytes .../fixtures/examples/knitr_utf8/output.pdf | Bin 75114 -> 75823 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/acceptance/fixtures/examples/asymptote/output.pdf b/test/acceptance/fixtures/examples/asymptote/output.pdf index 942b9154967cd1f79d9ad467cc431d0244d29b45..e8fdde84654c01a58bff2778dc94a1f894a1a3e7 100644 GIT binary patch delta 37016 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 delta 35867 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 diff --git a/test/acceptance/fixtures/examples/knitr/output.pdf b/test/acceptance/fixtures/examples/knitr/output.pdf index a1f93799e58349967efead8c4583661423ae3f23..7a839f7b1340dc097fead32bc2e3ad962da3d257 100644 GIT binary patch delta 3366 zcmbtW3piBk8W!a;NxCtj{0f`Y%&fVrnX%n+OSz0&QYdp-BgM=#CP_4QLOa@ubd+=< zj@pSfQE98RDdiH~WRfJIn=Wj1>~j9eCFiJpo_+SrGtaaBHQ##I|9=1bec$_}K{2mM zQEKFF@3;sd&~Qc<`?EgbD1ZcnoOL)03qW)U7GP{~a3sb92wVXwl|&~4q7(1~bOse5 zWCjQTL>8a}q645(XjBRyy2(Sm;fl^gfRz;v6Y%74;9DQ*xBmFM_i@xQOuRRXFhKP6 z#Q2a0CJ2(O1t{FCV{l076N07`5bZIpkcYXm#i3z9PTfuzvN2Q;2B?7OF2;DlT!~N& z2}4GJxB~J=02%%u`=tUN76ETi0GT<o<_qCJB;W~%o<gAn(1xzPU>X4hW)H%Je@+j1 zl;LO&h)eB(p9P)DVbG`yK8H>xk?1Iefs)t^4ilx3IV2hf<y%>BQ9hT;=22-BGL^wZ z$uus)<ufQe22UPHVOj;qQy(S~W9(2|#6d2H_b4ck6Ub0AOmB*rm_2QWOktilW+G+k zZq0NJL!OkNpq8(1V{1OqsjGk!$VpnI=6Us!lY^qd+c*_J`4J97{jb9uW%SoBM*07> z$NnrDCr|r`j@{WoSQsD=pURLH?AQ`^h%o50M5hgD6#eo@>LbETs0uGYK}Q8c`>f|L zNev8q6c`w&6{xTcd#Nr`z=~`Tb&(CCWT&B|uM|6T^1YXPvH0ib2Qpgl3Z>a9_r~Xu z{^f;GZ~_S?kc0uokX~G|mAqiKtx&El68xf2d~_yRE-|Q57&*$v6AKe=6mua%WLRrM z4CSoHRG=JpI3dIAh6@3zXslYATYOFnWF*a&zF8`~)5?fp@+QLs!lVOQEwk4;Q}Ahe zcu}Z#xGZWbYo1r(vIO6l*sRH>bLVc3ib`G*9HB6!$1HQ8G<0M13~=JwzN0NM8y0Wo z{!nd}I5AXZ)6Y{s3O42G+WUVzDa~IqCq7_BYpH&LFv;}k^aWRZ?q(WW8gEzeY2c<S z?95j34q>oED)#E)Z$+-f-OM<D+UjKS?8|9BU8;|g1rae9<AiblJgIwiLxXEqXIoK2 zPxCWBZ2Ku?CFL`3Czu?0yW@4odRkS3L#2VDgTs@+F1A;+`4+FvY2}8^iBz(oR8MwI z?fa<@`F=VJqT|FQy_g*RS`u!{3)Zuy{+4+GSCfDFxNDK$;$?^G5A<f~>G{SV$SUqN zY_v!h;}Z*PuK1$~@m2U)Kkn3x`gF9iQ)_>vvDK87DP_MJRyv2Kr>eL1%W9=;^2hqf z7nLbB7U5B8vca?I+Rh%@YTIsP8E%yF7cX9Ru0894!Ht197OQQ!<%ic1t9r`%ZsvI0 z8*Dnol3m&QW`e)6rP%}5+(uuI+9%he({x()5A+^Q+f`TQ(y^^x_F@qG{auBJ!3-)v z8QaFnsIBPKdAn{<aY}LHn{h?U{Br&yeUCPxZ3Ie(!W>zV0R=(j4gYhaln;cFo=HVu zJrqe5$(nP<;UjXDjuF^ca0(Ol(IGf8KuNUGq8bYhn;iob&7HAM_bcG?+8qNv3LxJ( z8Fum!a3%_i;43>vNGJsXMSX-lre8rP|0nt=0z{|K;B*^7r!i@xM8<9%Wx#P*^v?Iy z?5}{2nW*x@8-;`5bo%HDjuR3J>-%eaM;LH$6tN_>1p4^2j~{a^&N?qi=m-*Ez_C1B z@o+f97di%u!z6H+p<~Pb;7gwXmW(try0zeJaz<`dYxq2aV~Qtm>Q(Rex$D~^p~a1b zhH-+6Tkmba1$*Z1ysYavqqXU3|AThFH5sNyE>TH4cTK1>D%9LwQJ{LP?UHGpstO)Y z5a}gV5ho;hgiKo`O;bEoc&90AesCeJt8(ol1Gi(-M1QE|+PH6Dps~%szMG}1YZKXB z9Mc$B6KB)bbpQG!_xsHbZK0iK$#&YM6gvF}J*@lnZ(5?br*2cP9naJzT{@b=HLDA) zw)@?!M5C;8P3hF1bR6`T?$egFGn9@;8-9=K^Q)ux@CjWhU4Bx3Hr~<RjeoDQXG@~` zdw%(G``w98a`BylSy8@IUd4Xu7bYyZ7F}58QpX4_N^lKVS(Olw&$oWq$}H<L@5rso zZ@#?^&AW4;MofE`{O-o9`$pu$hX~KLPSn<}HuF1aZ2i3FWnW8H&#B9Gu4Wx=kuNUP zm1ow8)b|WjyZ$6yD(%@=n7(;ta>%Q~6-P*Z`t>r)*koFC>zO+q?knS$YtfBWPdnZ` zqi>!43GL9W&xlzeY~>dA;%6%>vJU^UA`vLgX|##;d0=3xlRo`B>SVmhQ_Za<ReAGs ze|~eK;Go}T?dDqd>wOV}8l4)}*AI8SX{b53RjavpowK`ns;9D8TCH{ae!Z%7_Op(9 zD_aBilUFUWEp6@9b9UtQYR8)tSv{MieqHf)ZGx7WX8f*7lky%JkGeXFvocJjBWQZ6 zE@!E2F3-HItYqb(q+CNIo$95F=awsMh-*04Em|l%M*ndM(`n87l9EQI7Ak3KvyHi% zzSpU2dZ*)->fb6J_f|=JuDq`+F?%DJ#!6vtGPe~a9Bognc$k@eI5IAOR*-mQ_wEQM z@3e3OYKpV1orku0!FsmF`DM;aL^HjfuKriey*s4ly|);(Yusb$d32v!#9Mr6?PIn5 zf#r?IjrvjNMqQVd`5(AN63bsx2)&P|<mdc@)<<YP_)OeAkaH@+U+SfP=)r3B_6v*R z>6Hru;(gX31r2ZOKZa*kUO8Nn-X!C&gEUs$=v;KpHE4fps@l^t<w(GR_8PYvA96Z^ ztUIi}I~QH(_25D!qv&q_oc((?Ki7ZrSn$f}Vn!M%zoo`LOij1+{NRAyACtU`O?Qni zRNpU6hG++!nIu>!uxCp!U}A4hCLweZ9U&nU3W_jRnL-09kAR-j#2fR4OIIQ9o3W`T z+13jgz+WB;T!O<fAcyle1?xp@$=WcC4Z<;T7<?N*31kAr6r>hp;jI}E)mzLKiJ-eD zI<iB;#+rpO4Uza2?+mTfa7X#See;kXhW`0)I9~3bSzZN2<ErJ4)m_w+taTJr%_m$L c@4x=bKtjXJ7E2<;7#~MQP@IvG1IrQjZ)V)PYybcN delta 2743 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 AHUIzs diff --git a/test/acceptance/fixtures/examples/knitr_utf8/output.pdf b/test/acceptance/fixtures/examples/knitr_utf8/output.pdf index 015941cc2019cf51de6b6405eb5cea68829d06ef..2d63d1af08b950f633c412658d756bb3c03cc078 100644 GIT binary patch literal 75823 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 literal 75114 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 From 472531f6173795e6cac3db8bc8efefc34f7a95d0 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 18 Jul 2017 11:29:59 +0100 Subject: [PATCH 221/709] fix exception for empty content in TikzManager --- app/coffee/TikzManager.coffee | 4 ++-- test/unit/coffee/TikzManager.coffee | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index cfa13321..5c08f205 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -22,8 +22,8 @@ module.exports = TikzManager = _includesTikz: (resource) -> # check if we are using tikz externalize - content = resource.content.slice(0,65536) - if content.indexOf("\\tikzexternalize") >= 0 + content = resource.content?.slice(0,65536) + if content?.indexOf("\\tikzexternalize") >= 0 return true else return false diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.coffee index 8174b4a2..816b3b10 100644 --- a/test/unit/coffee/TikzManager.coffee +++ b/test/unit/coffee/TikzManager.coffee @@ -30,6 +30,12 @@ describe 'TikzManager', -> { path: 'output.tex' } ]).should.equal false + it "should return false if the file has no content", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex' }, + { path: 'main.tex' } + ]).should.equal false + describe "injectOutputFile", -> beforeEach -> @rootDir = "/mock" From 11cd569ed95b0673388ecf9ba01f161de6600901 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 18 Jul 2017 11:30:22 +0100 Subject: [PATCH 222/709] stub out unwanted dependency in unit tests --- test/unit/coffee/CompileManagerTests.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 68629a1c..de331664 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -18,6 +18,7 @@ describe "CompileManager", -> "child_process": @child_process = {} "./CommandRunner": @CommandRunner = {} "./DraftModeManager": @DraftModeManager = {} + "./TikzManager": @TikzManager = {} "fs": @fs = {} @callback = sinon.stub() @@ -55,6 +56,7 @@ describe "CompileManager", -> @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) @DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) + @TikzManager.needsOutputFile = sinon.stub().returns(false) describe "normally", -> beforeEach -> From 104ce81ebdf41d88acd7fb6f2abf99fbc4eb91df Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Sun, 23 Jul 2017 22:42:07 +0100 Subject: [PATCH 223/709] change --- app.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app.coffee b/app.coffee index b99b2774..0e056fc1 100644 --- a/app.coffee +++ b/app.coffee @@ -1,3 +1,4 @@ + CompileController = require "./app/js/CompileController" Settings = require "settings-sharelatex" logger = require "logger-sharelatex" From e5081df2a9936ccbfa14b2c21d65c3430f08b137 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Sun, 23 Jul 2017 22:45:04 +0100 Subject: [PATCH 224/709] Revert "change" This reverts commit 104ce81ebdf41d88acd7fb6f2abf99fbc4eb91df. --- app.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app.coffee b/app.coffee index 0e056fc1..b99b2774 100644 --- a/app.coffee +++ b/app.coffee @@ -1,4 +1,3 @@ - CompileController = require "./app/js/CompileController" Settings = require "settings-sharelatex" logger = require "logger-sharelatex" From cd5adaff5151972066b21a3ac957ad41f9f1d31d Mon Sep 17 00:00:00 2001 From: Hayden Faulds <fauldsh@gmail.com> Date: Thu, 27 Jul 2017 14:02:24 +0100 Subject: [PATCH 225/709] keep compiles directory --- compiles/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 compiles/.gitkeep diff --git a/compiles/.gitkeep b/compiles/.gitkeep new file mode 100644 index 00000000..e69de29b From 4c105e7826ce344d62f025b7d20ea420d41b7671 Mon Sep 17 00:00:00 2001 From: Hayden Faulds <fauldsh@gmail.com> Date: Thu, 27 Jul 2017 15:54:20 +0100 Subject: [PATCH 226/709] keep cache directory --- cache/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cache/.gitkeep diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 00000000..e69de29b From 7e1d3d98e701eca3ea6ab507ed22c7c9f93d04bc Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 1 Aug 2017 14:35:55 +0100 Subject: [PATCH 227/709] write files incrementally --- app/coffee/CompileController.coffee | 5 ++++- app/coffee/CompileManager.coffee | 2 +- app/coffee/RequestParser.coffee | 6 +++++ app/coffee/ResourceWriter.coffee | 34 ++++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 250f9b8e..aefa70dd 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -15,7 +15,10 @@ module.exports = CompileController = ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? CompileManager.doCompile request, (error, outputFiles = []) -> - if error?.terminated + if error?.message is "invalid state" + code = 409 # Http 409 Conflict + status = "retry" + else if error?.terminated status = "terminated" else if error?.validate status = "validation-#{error.validate}" diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index ae40bf79..8673b52d 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -31,7 +31,7 @@ module.exports = CompileManager = timer = new Metrics.Timer("write-to-disk") 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) -> + ResourceWriter.syncResourcesToDisk request, compileDir, (error) -> if error? logger.err err:error, project_id: request.project_id, user_id: request.user_id, "error writing resources to disk" return callback(error) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 8fc4ecf3..fe22982f 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -31,6 +31,12 @@ module.exports = RequestParser = response.check = @_parseAttribute "check", compile.options.check, type: "string" + response.incremental = @_parseAttribute "incremental", + compile.options.incremental, + type: "string" + response.state = @_parseAttribute "state", + compile.options.state, + type: "string" if response.timeout > RequestParser.MAX_TIMEOUT response.timeout = RequestParser.MAX_TIMEOUT diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index e2a0e1f8..a4ae4251 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -11,7 +11,39 @@ settings = require("settings-sharelatex") parallelFileDownloads = settings.parallelFileDownloads or 1 module.exports = ResourceWriter = - syncResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> + + syncResourcesToDisk: (request, basePath, callback = (error) ->) -> + if request.incremental? + ResourceWriter.checkState request.incremental, basePath, (error, ok) -> + logger.log state: request.state, result:ok, "checked state on incremental request" + return callback new Error("invalid state") if not ok + ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, callback + else + @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> + return callback(error) if error? + ResourceWriter.storeState request.state, basePath, callback + + storeState: (state, basePath, callback) -> + logger.log state:state, basePath:basePath, "writing state" + fs.writeFile Path.join(basePath, ".resource-state"), state, {encoding: 'ascii'}, callback + + checkState: (state, basePath, callback) -> + fs.readFile Path.join(basePath, ".resource-state"), {encoding:'ascii'}, (err, oldState) -> + logger.log state:state, oldState: oldState, basePath:basePath, err:err, "checking state" + if state is oldState + return callback(null, true) + else + return callback(null, false) + + saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> + @_createDirectory basePath, (error) => + return callback(error) if error? + jobs = for resource in resources + do (resource) => + (callback) => @_writeResourceToDisk(project_id, resource, basePath, callback) + async.parallelLimit jobs, parallelFileDownloads, callback + + saveAllResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @_createDirectory basePath, (error) => return callback(error) if error? @_removeExtraneousFiles resources, basePath, (error) => From 74c26120b2e9d6989759987018328e9dcd4d9a63 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 3 Aug 2017 11:51:58 +0100 Subject: [PATCH 228/709] use syncType and syncState for clsi state options --- app/coffee/RequestParser.coffee | 8 ++++---- app/coffee/ResourceWriter.coffee | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index fe22982f..d2e67bca 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -31,11 +31,11 @@ module.exports = RequestParser = response.check = @_parseAttribute "check", compile.options.check, type: "string" - response.incremental = @_parseAttribute "incremental", - compile.options.incremental, + response.syncType = @_parseAttribute "syncType", + compile.options.syncType, type: "string" - response.state = @_parseAttribute "state", - compile.options.state, + response.syncState = @_parseAttribute "syncState", + compile.options.syncState, type: "string" if response.timeout > RequestParser.MAX_TIMEOUT diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index a4ae4251..fca2c436 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -13,23 +13,23 @@ parallelFileDownloads = settings.parallelFileDownloads or 1 module.exports = ResourceWriter = syncResourcesToDisk: (request, basePath, callback = (error) ->) -> - if request.incremental? - ResourceWriter.checkState request.incremental, basePath, (error, ok) -> - logger.log state: request.state, result:ok, "checked state on incremental request" + if request.syncType? is "incremental" + ResourceWriter.checkSyncState request.syncState, basePath, (error, ok) -> + logger.log syncState: request.syncState, result:ok, "checked state on incremental request" return callback new Error("invalid state") if not ok ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, callback else @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? - ResourceWriter.storeState request.state, basePath, callback + ResourceWriter.storeSyncState request.syncState, basePath, callback - storeState: (state, basePath, callback) -> - logger.log state:state, basePath:basePath, "writing state" - fs.writeFile Path.join(basePath, ".resource-state"), state, {encoding: 'ascii'}, callback + storeSyncState: (state, basePath, callback) -> + logger.log state:state, basePath:basePath, "writing sync state" + fs.writeFile Path.join(basePath, ".resource-sync-state"), state, {encoding: 'ascii'}, callback - checkState: (state, basePath, callback) -> - fs.readFile Path.join(basePath, ".resource-state"), {encoding:'ascii'}, (err, oldState) -> - logger.log state:state, oldState: oldState, basePath:basePath, err:err, "checking state" + checkSyncState: (state, basePath, callback) -> + fs.readFile Path.join(basePath, ".resource-sync-state"), {encoding:'ascii'}, (err, oldState) -> + logger.log state:state, oldState: oldState, basePath:basePath, err:err, "checking sync state" if state is oldState return callback(null, true) else From 11898b897e4e495d5ce35da66d15fb29eec6d5b1 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 3 Aug 2017 15:56:59 +0100 Subject: [PATCH 229/709] added files out of sync error object --- app/coffee/CompileController.coffee | 3 ++- app/coffee/Errors.coffee | 7 +++++++ app/coffee/ResourceWriter.coffee | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index aefa70dd..6f93bfea 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -4,6 +4,7 @@ Settings = require "settings-sharelatex" Metrics = require "./Metrics" ProjectPersistenceManager = require "./ProjectPersistenceManager" logger = require "logger-sharelatex" +Errors = require "./Errors" module.exports = CompileController = compile: (req, res, next = (error) ->) -> @@ -15,7 +16,7 @@ module.exports = CompileController = ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? CompileManager.doCompile request, (error, outputFiles = []) -> - if error?.message is "invalid state" + if error instanceOf Errors.FilesOutOfSyncError code = 409 # Http 409 Conflict status = "retry" else if error?.terminated diff --git a/app/coffee/Errors.coffee b/app/coffee/Errors.coffee index 4abea0bb..2e3ae759 100644 --- a/app/coffee/Errors.coffee +++ b/app/coffee/Errors.coffee @@ -5,6 +5,13 @@ NotFoundError = (message) -> return error NotFoundError.prototype.__proto__ = Error.prototype +FilesOutOfSyncError = (message) -> + error = new Error(message) + error.name = "FilesOutOfSyncError" + error.__proto__ = FilesOutOfSyncError.prototype + return error +FilesOutOfSyncError.prototype.__proto__ = Error.prototype module.exports = Errors = NotFoundError: NotFoundError + FilesOutOfSyncError: FilesOutOfSyncError diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index fca2c436..725d0699 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -5,6 +5,7 @@ async = require "async" mkdirp = require "mkdirp" OutputFileFinder = require "./OutputFileFinder" Metrics = require "./Metrics" +Errors = require "./Errors" logger = require "logger-sharelatex" settings = require("settings-sharelatex") @@ -16,7 +17,7 @@ module.exports = ResourceWriter = if request.syncType? is "incremental" ResourceWriter.checkSyncState request.syncState, basePath, (error, ok) -> logger.log syncState: request.syncState, result:ok, "checked state on incremental request" - return callback new Error("invalid state") if not ok + return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") if not ok ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, callback else @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> From b4be40d0619823c455955a25f5eb581e16f3df7a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 7 Aug 2017 10:19:56 +0100 Subject: [PATCH 230/709] restrict syncType values to full/incremental --- app/coffee/RequestParser.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index d2e67bca..8b8de76e 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -33,6 +33,7 @@ module.exports = RequestParser = type: "string" response.syncType = @_parseAttribute "syncType", compile.options.syncType, + validValues: ["full", "incremental"] type: "string" response.syncState = @_parseAttribute "syncState", compile.options.syncState, From 6542ce20b6b91b98acf5e017d533e8696d337830 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 7 Aug 2017 14:26:13 +0100 Subject: [PATCH 231/709] fix incremental request --- app/coffee/CompileController.coffee | 2 +- app/coffee/CompileManager.coffee | 7 +++++-- app/coffee/ResourceWriter.coffee | 15 ++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 6f93bfea..f4dcb0ec 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -16,7 +16,7 @@ module.exports = CompileController = ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? CompileManager.doCompile request, (error, outputFiles = []) -> - if error instanceOf Errors.FilesOutOfSyncError + if error instanceof Errors.FilesOutOfSyncError code = 409 # Http 409 Conflict status = "retry" else if error?.terminated diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 8673b52d..2b563301 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -32,9 +32,12 @@ module.exports = CompileManager = timer = new Metrics.Timer("write-to-disk") logger.log project_id: request.project_id, user_id: request.user_id, "syncing resources to disk" ResourceWriter.syncResourcesToDisk request, compileDir, (error) -> - if error? + if error? and error instanceof Errors.FilesOutOfSyncError + logger.warn project_id: request.project_id, user_id: request.user_id, "files out of sync, please retry" + return callback(error) + else if error? logger.err err:error, project_id: request.project_id, user_id: request.user_id, "error writing resources to disk" - return callback(error) + return callback(error) logger.log project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start, "written files to disk" timer.done() diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 725d0699..29e0e576 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -14,10 +14,10 @@ parallelFileDownloads = settings.parallelFileDownloads or 1 module.exports = ResourceWriter = syncResourcesToDisk: (request, basePath, callback = (error) ->) -> - if request.syncType? is "incremental" - ResourceWriter.checkSyncState request.syncState, basePath, (error, ok) -> - logger.log syncState: request.syncState, result:ok, "checked state on incremental request" - return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") if not ok + if request.syncType is "incremental" + ResourceWriter.checkSyncState request.syncState, basePath, (error, syncStateOk) -> + logger.log syncState: request.syncState, result:syncStateOk, "checked state on incremental request" + return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") if not syncStateOk ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, callback else @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> @@ -30,11 +30,8 @@ module.exports = ResourceWriter = checkSyncState: (state, basePath, callback) -> fs.readFile Path.join(basePath, ".resource-sync-state"), {encoding:'ascii'}, (err, oldState) -> - logger.log state:state, oldState: oldState, basePath:basePath, err:err, "checking sync state" - if state is oldState - return callback(null, true) - else - return callback(null, false) + # ignore errors, return true if state matches, false otherwise (including errors) + return callback(null, if state is oldState then true else false) saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @_createDirectory basePath, (error) => From 206adc2d048fe0985782af55414e07bb8dc71ad1 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 7 Aug 2017 15:00:16 +0100 Subject: [PATCH 232/709] fix broken unit tests --- test/unit/coffee/CompileManagerTests.coffee | 4 ++-- test/unit/coffee/ResourceWriterTests.coffee | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index de331664..25109b62 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -51,7 +51,7 @@ describe "CompileManager", -> @env = {} @Settings.compileDir = "compiles" @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" - @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArg(3) + @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArg(2) @LatexRunner.runLatex = sinon.stub().callsArg(2) @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) @@ -64,7 +64,7 @@ describe "CompileManager", -> it "should write the resources to disk", -> @ResourceWriter.syncResourcesToDisk - .calledWith(@project_id, @resources, @compileDir) + .calledWith(@request, @compileDir) .should.equal true it "should run LaTeX", -> diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index fd1ae309..efaf36a3 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -29,7 +29,9 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceWriter.syncResourcesToDisk(@project_id, @resources, @basePath, @callback) + @ResourceWriter.checkSyncState = sinon.stub().callsArg(2) + @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) + @ResourceWriter.syncResourcesToDisk({project_id: @project_id, resources: @resources}, @basePath, @callback) it "should remove old files", -> @ResourceWriter._removeExtraneousFiles From c3fe17d0b612137790566b186d64362bf580ac7a Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 7 Aug 2017 15:29:18 +0100 Subject: [PATCH 233/709] Revert "Keep compiles and cache directories" --- cache/.gitkeep | 0 compiles/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cache/.gitkeep delete mode 100644 compiles/.gitkeep diff --git a/cache/.gitkeep b/cache/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/compiles/.gitkeep b/compiles/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 7cd81ac3df8837c3b30f6b824e858dc8e6415a2f Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 7 Aug 2017 16:21:37 +0100 Subject: [PATCH 234/709] use grunt to make compiles and cache dirs --- Gruntfile.coffee | 8 +++++++- package.json | 17 +++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 09d37346..a30f141e 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -46,6 +46,11 @@ module.exports = (grunt) -> app: src: "app.js" + mkdir: + all: + options: + create: ["cache", "compiles"] + mochaTest: unit: options: @@ -70,6 +75,7 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-shell' grunt.loadNpmTasks 'grunt-execute' grunt.loadNpmTasks 'grunt-bunyan' + grunt.loadNpmTasks 'grunt-mkdir' grunt.registerTask 'compile:bin', () -> callback = @async() @@ -93,6 +99,6 @@ module.exports = (grunt) -> grunt.registerTask 'install', 'compile:app' - grunt.registerTask 'default', ['run'] + grunt.registerTask 'default', ['mkdir', 'run'] diff --git a/package.json b/package.json index 73bb08af..56843026 100644 --- a/package.json +++ b/package.json @@ -9,23 +9,24 @@ "author": "James Allen <james@sharelatex.com>", "dependencies": { "async": "0.2.9", + "body-parser": "^1.2.0", + "express": "^4.2.0", + "fs-extra": "^0.16.3", + "grunt-mkdir": "^1.0.0", + "heapdump": "^0.3.5", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", "sequelize": "^2.1.3", - "wrench": "~1.5.4", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", "sqlite3": "~3.1.8", - "express": "^4.2.0", - "body-parser": "^1.2.0", - "fs-extra": "^0.16.3", "underscore": "^1.8.2", "v8-profiler": "^5.2.4", - "heapdump": "^0.3.5" + "wrench": "~1.5.4" }, "devDependencies": { "mocha": "1.10.0", From 86fa940c97deb7e4cbce5e1673de59828167beab Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 8 Aug 2017 16:27:53 +0100 Subject: [PATCH 235/709] clean up the state file if no state passed in --- app/coffee/ResourceWriter.coffee | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 29e0e576..ad14b917 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -25,11 +25,21 @@ module.exports = ResourceWriter = ResourceWriter.storeSyncState request.syncState, basePath, callback storeSyncState: (state, basePath, callback) -> - logger.log state:state, basePath:basePath, "writing sync state" - fs.writeFile Path.join(basePath, ".resource-sync-state"), state, {encoding: 'ascii'}, callback + stateFile = Path.join(basePath, ".resource-sync-state") + if not state? # remove the file if no state passed in + logger.log state:state, basePath:basePath, "clearing sync state" + fs.unlink stateFile, (err) -> + if err? and err.code isnt 'ENOENT' + return callback(err) + else + return callback() + else + logger.log state:state, basePath:basePath, "writing sync state" + fs.writeFile stateFile, state, {encoding: 'ascii'}, callback checkSyncState: (state, basePath, callback) -> - fs.readFile Path.join(basePath, ".resource-sync-state"), {encoding:'ascii'}, (err, oldState) -> + stateFile = Path.join(basePath, ".resource-sync-state") + fs.readFile stateFile, {encoding:'ascii'}, (err, oldState) -> # ignore errors, return true if state matches, false otherwise (including errors) return callback(null, if state is oldState then true else false) From c25e96bbc3f62cf3c8836ad28fbfc5509e8cbe5f Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 9 Aug 2017 15:10:24 +0100 Subject: [PATCH 236/709] add comment about syncType/syncState --- app/coffee/RequestParser.coffee | 14 ++++++++++++++ app/coffee/ResourceWriter.coffee | 26 ++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 8b8de76e..596b5299 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -31,10 +31,24 @@ module.exports = RequestParser = response.check = @_parseAttribute "check", compile.options.check, type: "string" + + # The syncType specifies whether the request contains all + # resources (full) or only those resources to be updated + # in-place (incremental). response.syncType = @_parseAttribute "syncType", compile.options.syncType, validValues: ["full", "incremental"] type: "string" + + # The syncState is an identifier passed in with the request + # which has the property that it changes when any resource is + # added, deleted, moved or renamed. + # + # on syncType full the syncState identifier is passed in and + # stored + # + # on syncType incremental the syncState identifier must match + # the stored value response.syncState = @_parseAttribute "syncState", compile.options.syncState, type: "string" diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index ad14b917..9a78671e 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -24,8 +24,23 @@ module.exports = ResourceWriter = return callback(error) if error? ResourceWriter.storeSyncState request.syncState, basePath, callback + # The sync state is an identifier which must match for an + # incremental update to be allowed. + # + # The initial value is passed in and stored on a full + # compile. + # + # Subsequent incremental compiles must come with the same value - if + # not they will be rejected with a 409 Conflict response. + # + # An incremental compile can only update existing files with new + # content. The sync state identifier must change if any docs or + # files are moved, added, deleted or renamed. + + SYNC_STATE_FILE: ".project-sync-state" + storeSyncState: (state, basePath, callback) -> - stateFile = Path.join(basePath, ".resource-sync-state") + stateFile = Path.join(basePath, @SYNC_STATE_FILE) if not state? # remove the file if no state passed in logger.log state:state, basePath:basePath, "clearing sync state" fs.unlink stateFile, (err) -> @@ -38,10 +53,13 @@ module.exports = ResourceWriter = fs.writeFile stateFile, state, {encoding: 'ascii'}, callback checkSyncState: (state, basePath, callback) -> - stateFile = Path.join(basePath, ".resource-sync-state") + stateFile = Path.join(basePath, @SYNC_STATE_FILE) fs.readFile stateFile, {encoding:'ascii'}, (err, oldState) -> - # ignore errors, return true if state matches, false otherwise (including errors) - return callback(null, if state is oldState then true else false) + if err? and err.code isnt 'ENOENT' + return callback(err) + else + # return true if state matches, false otherwise (including file not existing) + callback(null, if state is oldState then true else false) saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @_createDirectory basePath, (error) => From 00ddfdf42bce5807f614bdd55eb0877ebd48ac55 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 9 Aug 2017 15:22:44 +0100 Subject: [PATCH 237/709] fix unit tests --- test/unit/coffee/ResourceWriterTests.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index efaf36a3..d4dd9c16 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -7,7 +7,9 @@ path = require "path" describe "ResourceWriter", -> beforeEach -> @ResourceWriter = SandboxedModule.require modulePath, requires: - "fs": @fs = { mkdir: sinon.stub().callsArg(1) } + "fs": @fs = + mkdir: sinon.stub().callsArg(1) + unlink: sinon.stub().callsArg(1) "wrench": @wrench = {} "./UrlCache" : @UrlCache = {} "mkdirp" : @mkdirp = sinon.stub().callsArg(1) From 2b610030d54b9be2f1302def001e70464e7492c7 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 17 Aug 2017 14:53:35 +0100 Subject: [PATCH 238/709] store the resource list in a file --- app/coffee/CompileManager.coffee | 13 ++++++----- app/coffee/ResourceListManager.coffee | 24 +++++++++++++++++++++ app/coffee/ResourceWriter.coffee | 13 ++++++++--- test/unit/coffee/CompileManagerTests.coffee | 2 +- test/unit/coffee/ResourceWriterTests.coffee | 3 +++ 5 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 app/coffee/ResourceListManager.coffee diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 2b563301..d6c79da0 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -31,7 +31,8 @@ module.exports = CompileManager = timer = new Metrics.Timer("write-to-disk") logger.log project_id: request.project_id, user_id: request.user_id, "syncing resources to disk" - ResourceWriter.syncResourcesToDisk request, compileDir, (error) -> + ResourceWriter.syncResourcesToDisk request, compileDir, (error, resourceList) -> + # NOTE: resourceList is insecure, it should only be used to exclude files from the output list if error? and error instanceof Errors.FilesOutOfSyncError logger.warn project_id: request.project_id, user_id: request.user_id, "files out of sync, please retry" return callback(error) @@ -40,15 +41,17 @@ module.exports = CompileManager = return callback(error) logger.log project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start, "written files to disk" timer.done() - + + # FIXME - for incremental compiles we don't want to inject this multiple times injectDraftModeIfRequired = (callback) -> if request.draft DraftModeManager.injectDraftMode Path.join(compileDir, request.rootResourcePath), callback else callback() + # FIXME - for incremental compiles we may need to update output.tex every time createTikzFileIfRequired = (callback) -> - if TikzManager.needsOutputFile(request.rootResourcePath, request.resources) + if TikzManager.needsOutputFile(request.rootResourcePath, resourceList) TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback else callback() @@ -94,7 +97,7 @@ module.exports = CompileManager = 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) -> + OutputFileFinder.findOutputFiles resourceList, compileDir, (err, outputFiles) -> return callback(err) if err? callback(error, outputFiles) # return output files so user can check logs return @@ -114,7 +117,7 @@ module.exports = CompileManager = if stats?["latex-runs"] > 0 and timings?["cpu-time"] > 0 Metrics.timing("run-compile-cpu-time-per-pass", timings["cpu-time"] / stats["latex-runs"]) - OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) -> + OutputFileFinder.findOutputFiles resourceList, compileDir, (error, outputFiles) -> return callback(error) if error? OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) -> callback null, newOutputFiles diff --git a/app/coffee/ResourceListManager.coffee b/app/coffee/ResourceListManager.coffee new file mode 100644 index 00000000..d68f5d1a --- /dev/null +++ b/app/coffee/ResourceListManager.coffee @@ -0,0 +1,24 @@ +Path = require "path" +fs = require "fs" +mkdirp = require "mkdirp" +logger = require "logger-sharelatex" +settings = require("settings-sharelatex") + +module.exports = ResourceListManager = + + # This file is a list of the input files for the project, one per + # line, used to identify output files (i.e. files not on this list) + # when the incoming request is incremental. + RESOURCE_LIST_FILE: ".project-resource-list" + + saveResourceList: (resources, basePath, callback = (error) ->) -> + resourceListFile = Path.join(basePath, @RESOURCE_LIST_FILE) + resourceList = (resource.path for resource in resources) + fs.writeFile resourceListFile, resourceList.join("\n"), callback + + loadResourceList: (basePath, callback = (error) ->) -> + resourceListFile = Path.join(basePath, @RESOURCE_LIST_FILE) + fs.readFile resourceListFile, (err, resourceList) -> + return callback(err) if err? + resources = ({path: path} for path in resourceList?.toString()?.split("\n") or []) + callback(null, resources) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 9a78671e..18b8122e 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -4,6 +4,7 @@ fs = require "fs" async = require "async" mkdirp = require "mkdirp" OutputFileFinder = require "./OutputFileFinder" +ResourceListManager = require "./ResourceListManager" Metrics = require "./Metrics" Errors = require "./Errors" logger = require "logger-sharelatex" @@ -13,16 +14,22 @@ parallelFileDownloads = settings.parallelFileDownloads or 1 module.exports = ResourceWriter = - syncResourcesToDisk: (request, basePath, callback = (error) ->) -> + syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) -> if request.syncType is "incremental" ResourceWriter.checkSyncState request.syncState, basePath, (error, syncStateOk) -> logger.log syncState: request.syncState, result:syncStateOk, "checked state on incremental request" return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") if not syncStateOk - ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, callback + ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> + return callback(error) if error? + ResourceListManager.loadResourceList basePath, callback else @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? - ResourceWriter.storeSyncState request.syncState, basePath, callback + ResourceWriter.storeSyncState request.syncState, basePath, (error) -> + return callback(error) if error? + ResourceListManager.saveResourceList request.resources, basePath, (error) => + return callback(error) if error? + callback(null, request.resources) # The sync state is an identifier which must match for an # incremental update to be allowed. diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 25109b62..ff671b27 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -51,7 +51,7 @@ describe "CompileManager", -> @env = {} @Settings.compileDir = "compiles" @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" - @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArg(2) + @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArgWith(2, null, @resources) @LatexRunner.runLatex = sinon.stub().callsArg(2) @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index d4dd9c16..0e602502 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -10,6 +10,7 @@ describe "ResourceWriter", -> "fs": @fs = mkdir: sinon.stub().callsArg(1) unlink: sinon.stub().callsArg(1) + "./ResourceListManager": @ResourceListManager = {} "wrench": @wrench = {} "./UrlCache" : @UrlCache = {} "mkdirp" : @mkdirp = sinon.stub().callsArg(1) @@ -33,6 +34,8 @@ describe "ResourceWriter", -> @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) @ResourceWriter.checkSyncState = sinon.stub().callsArg(2) @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) + @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) + @ResourceListManager.loadResourceList = sinon.stub().callsArg(1) @ResourceWriter.syncResourcesToDisk({project_id: @project_id, resources: @resources}, @basePath, @callback) it "should remove old files", -> From 5b5f7b06905aea03541ba4814d3038df26520604 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 17 Aug 2017 15:03:37 +0100 Subject: [PATCH 239/709] avoid adding draft mode more than once --- app/coffee/CompileManager.coffee | 2 -- app/coffee/DraftModeManager.coffee | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index d6c79da0..56d04db7 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -42,14 +42,12 @@ module.exports = CompileManager = logger.log project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start, "written files to disk" timer.done() - # FIXME - for incremental compiles we don't want to inject this multiple times injectDraftModeIfRequired = (callback) -> if request.draft DraftModeManager.injectDraftMode Path.join(compileDir, request.rootResourcePath), callback else callback() - # FIXME - for incremental compiles we may need to update output.tex every time createTikzFileIfRequired = (callback) -> if TikzManager.needsOutputFile(request.rootResourcePath, resourceList) TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback diff --git a/app/coffee/DraftModeManager.coffee b/app/coffee/DraftModeManager.coffee index 253cbffd..2f9e931c 100644 --- a/app/coffee/DraftModeManager.coffee +++ b/app/coffee/DraftModeManager.coffee @@ -5,6 +5,9 @@ module.exports = DraftModeManager = injectDraftMode: (filename, callback = (error) ->) -> fs.readFile filename, "utf8", (error, content) -> return callback(error) if error? + # avoid adding draft mode more than once + if content?.indexOf("\\documentclass\[draft") >= 0 + return callback() modified_content = DraftModeManager._injectDraftOption content logger.log { content: content.slice(0,1024), # \documentclass is normally v near the top @@ -18,4 +21,4 @@ module.exports = DraftModeManager = # With existing options (must be first, otherwise both are applied) .replace(/\\documentclass\[/g, "\\documentclass[draft,") # Without existing options - .replace(/\\documentclass\{/g, "\\documentclass[draft]{") \ No newline at end of file + .replace(/\\documentclass\{/g, "\\documentclass[draft]{") From a8aaf58e64560bc1d0fbf8e246fa4fe1ed9690ea Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 17 Aug 2017 15:57:05 +0100 Subject: [PATCH 240/709] test syncType in RequestParser --- test/unit/coffee/RequestParserTests.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index 1cd931bc..0b420b36 100644 --- a/test/unit/coffee/RequestParserTests.coffee +++ b/test/unit/coffee/RequestParserTests.coffee @@ -242,3 +242,13 @@ describe "RequestParser", -> it "should return an error", -> @callback.calledWith("relative path in root resource") .should.equal true + + describe "with an unknown syncType", -> + beforeEach -> + @validRequest.compile.options.syncType = "unexpected" + @RequestParser.parse @validRequest, @callback + @data = @callback.args[0][1] + + it "should return an error", -> + @callback.calledWith("syncType attribute should be one of: full, incremental") + .should.equal true From e4aad90f338becac7d8b3e7452a2679e654c061e Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 17 Aug 2017 16:59:37 +0100 Subject: [PATCH 241/709] ResourceWriter unit tests (wip) --- app/coffee/ResourceWriter.coffee | 11 +++- test/unit/coffee/ResourceWriterTests.coffee | 56 ++++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 18b8122e..d0d19882 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -19,9 +19,14 @@ module.exports = ResourceWriter = ResourceWriter.checkSyncState request.syncState, basePath, (error, syncStateOk) -> logger.log syncState: request.syncState, result:syncStateOk, "checked state on incremental request" return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") if not syncStateOk - ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> + ResourceListManager.loadResourceList basePath, (error, resourceList) -> return callback(error) if error? - ResourceListManager.loadResourceList basePath, callback + ResourceWriter._removeExtraneousFiles resourceList, basePath, (error) => + return callback(error) if error? + ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> + return callback(error) if error? + callback(null, resourceList) + else @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? @@ -113,6 +118,8 @@ module.exports = ResourceWriter = should_delete = true if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false + if path in ['.project-resource-list', '.project-sync-state'] + should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" should_delete = true if path == "output.tex" # created by TikzManager if present in output files diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 0e602502..32ffb132 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -23,7 +23,7 @@ describe "ResourceWriter", -> @basePath = "/path/to/write/files/to" @callback = sinon.stub() - describe "syncResourcesToDisk", -> + describe "syncResourcesToDisk on a full request", -> beforeEach -> @resources = [ "resource-1-mock" @@ -36,7 +36,59 @@ describe "ResourceWriter", -> @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) @ResourceListManager.loadResourceList = sinon.stub().callsArg(1) - @ResourceWriter.syncResourcesToDisk({project_id: @project_id, resources: @resources}, @basePath, @callback) + @ResourceWriter.syncResourcesToDisk({ + project_id: @project_id + syncState: @syncState = "0123456789abcdef" + resources: @resources + }, @basePath, @callback) + + it "should remove old files", -> + @ResourceWriter._removeExtraneousFiles + .calledWith(@resources, @basePath) + .should.equal true + + it "should write each resource to disk", -> + for resource in @resources + @ResourceWriter._writeResourceToDisk + .calledWith(@project_id, resource, @basePath) + .should.equal true + + it "should store the sync state", -> + console.log "CHECKING", @syncState, @basePath + @ResourceWriter.storeSyncState + .calledWith(@syncState, @basePath) + .should.equal true + + it "should save the resource list", -> + @ResourceListManager.saveResourceList + .calledWith(@resources, @basePath) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true + + describe "syncResourcesToDisk on an incremental update", -> + beforeEach -> + @resources = [ + "resource-1-mock" + ] + @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) + @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) + @ResourceWriter.checkSyncState = sinon.stub().callsArgWith(2, null, true) + @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) + @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) + @ResourceListManager.loadResourceList = sinon.stub().callsArg(1) + @ResourceWriter.syncResourcesToDisk({ + project_id: @project_id, + syncType: "incremental", + syncState: @syncState = "1234567890abcdef", + resources: @resources + }, @basePath, @callback) + + it "should check the sync state matches", -> + @ResourceWriter.checkSyncState + .calledWith(@syncState, @basePath) + .should.equal true it "should remove old files", -> @ResourceWriter._removeExtraneousFiles From e8064f12a12e5ab2844520db45cccdb785895dfe Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 18 Aug 2017 09:41:43 +0100 Subject: [PATCH 242/709] finish unit test for incremental update --- test/unit/coffee/ResourceWriterTests.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 32ffb132..9a17b05c 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -54,7 +54,6 @@ describe "ResourceWriter", -> .should.equal true it "should store the sync state", -> - console.log "CHECKING", @syncState, @basePath @ResourceWriter.storeSyncState .calledWith(@syncState, @basePath) .should.equal true @@ -77,7 +76,7 @@ describe "ResourceWriter", -> @ResourceWriter.checkSyncState = sinon.stub().callsArgWith(2, null, true) @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) - @ResourceListManager.loadResourceList = sinon.stub().callsArg(1) + @ResourceListManager.loadResourceList = sinon.stub().callsArgWith(1, null, @resources) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id, syncType: "incremental", From 0b9ddb8efe323e90bc336631b15a3850b3503375 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 18 Aug 2017 09:41:59 +0100 Subject: [PATCH 243/709] fix whitespace --- app/coffee/ResourceWriter.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index d0d19882..fae570f8 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -53,7 +53,7 @@ module.exports = ResourceWriter = storeSyncState: (state, basePath, callback) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) - if not state? # remove the file if no state passed in + if not state? # remove the file if no state passed in logger.log state:state, basePath:basePath, "clearing sync state" fs.unlink stateFile, (err) -> if err? and err.code isnt 'ENOENT' From 6921cf25b896d5587367a8a0009b657ab684d693 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 18 Aug 2017 10:22:17 +0100 Subject: [PATCH 244/709] splice state management into ResourceStateManager --- app/coffee/ResourceStateManager.coffee | 46 ++++++++++++++++++++ app/coffee/ResourceWriter.coffee | 47 ++------------------- test/unit/coffee/ResourceWriterTests.coffee | 35 ++++++++++++--- 3 files changed, 79 insertions(+), 49 deletions(-) create mode 100644 app/coffee/ResourceStateManager.coffee diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee new file mode 100644 index 00000000..2a6d177b --- /dev/null +++ b/app/coffee/ResourceStateManager.coffee @@ -0,0 +1,46 @@ +Path = require "path" +fs = require "fs" +mkdirp = require "mkdirp" +logger = require "logger-sharelatex" +settings = require("settings-sharelatex") +Errors = require "./Errors" + +module.exports = ResourceStateManager = + + # The sync state is an identifier which must match for an + # incremental update to be allowed. + # + # The initial value is passed in and stored on a full + # compile. + # + # Subsequent incremental compiles must come with the same value - if + # not they will be rejected with a 409 Conflict response. + # + # An incremental compile can only update existing files with new + # content. The sync state identifier must change if any docs or + # files are moved, added, deleted or renamed. + + SYNC_STATE_FILE: ".project-sync-state" + + saveProjectStateHash: (state, basePath, callback) -> + stateFile = Path.join(basePath, @SYNC_STATE_FILE) + if not state? # remove the file if no state passed in + logger.log state:state, basePath:basePath, "clearing sync state" + fs.unlink stateFile, (err) -> + if err? and err.code isnt 'ENOENT' + return callback(err) + else + return callback() + else + logger.log state:state, basePath:basePath, "writing sync state" + fs.writeFile stateFile, state, {encoding: 'ascii'}, callback + + checkProjectStateHashMatches: (state, basePath, callback) -> + stateFile = Path.join(basePath, @SYNC_STATE_FILE) + fs.readFile stateFile, {encoding:'ascii'}, (err, oldState) -> + if err? and err.code isnt 'ENOENT' + return callback(err) + else if state isnt oldState + return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") + else if state is oldState + callback(null) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index fae570f8..7f670262 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -4,9 +4,9 @@ fs = require "fs" async = require "async" mkdirp = require "mkdirp" OutputFileFinder = require "./OutputFileFinder" +ResourceStateManager = require "./ResourceStateManager" ResourceListManager = require "./ResourceListManager" Metrics = require "./Metrics" -Errors = require "./Errors" logger = require "logger-sharelatex" settings = require("settings-sharelatex") @@ -16,9 +16,8 @@ module.exports = ResourceWriter = syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) -> if request.syncType is "incremental" - ResourceWriter.checkSyncState request.syncState, basePath, (error, syncStateOk) -> - logger.log syncState: request.syncState, result:syncStateOk, "checked state on incremental request" - return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") if not syncStateOk + ResourceStateManager.checkProjectStateHashMatches request.syncState, basePath, (error) -> + return callback(error) if error? ResourceListManager.loadResourceList basePath, (error, resourceList) -> return callback(error) if error? ResourceWriter._removeExtraneousFiles resourceList, basePath, (error) => @@ -26,53 +25,15 @@ module.exports = ResourceWriter = ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? callback(null, resourceList) - else @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? - ResourceWriter.storeSyncState request.syncState, basePath, (error) -> + ResourceStateManager.saveProjectStateHash request.syncState, basePath, (error) -> return callback(error) if error? ResourceListManager.saveResourceList request.resources, basePath, (error) => return callback(error) if error? callback(null, request.resources) - # The sync state is an identifier which must match for an - # incremental update to be allowed. - # - # The initial value is passed in and stored on a full - # compile. - # - # Subsequent incremental compiles must come with the same value - if - # not they will be rejected with a 409 Conflict response. - # - # An incremental compile can only update existing files with new - # content. The sync state identifier must change if any docs or - # files are moved, added, deleted or renamed. - - SYNC_STATE_FILE: ".project-sync-state" - - storeSyncState: (state, basePath, callback) -> - stateFile = Path.join(basePath, @SYNC_STATE_FILE) - if not state? # remove the file if no state passed in - logger.log state:state, basePath:basePath, "clearing sync state" - fs.unlink stateFile, (err) -> - if err? and err.code isnt 'ENOENT' - return callback(err) - else - return callback() - else - logger.log state:state, basePath:basePath, "writing sync state" - fs.writeFile stateFile, state, {encoding: 'ascii'}, callback - - checkSyncState: (state, basePath, callback) -> - stateFile = Path.join(basePath, @SYNC_STATE_FILE) - fs.readFile stateFile, {encoding:'ascii'}, (err, oldState) -> - if err? and err.code isnt 'ENOENT' - return callback(err) - else - # return true if state matches, false otherwise (including file not existing) - callback(null, if state is oldState then true else false) - saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @_createDirectory basePath, (error) => return callback(error) if error? diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 9a17b05c..0804438f 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -11,6 +11,7 @@ describe "ResourceWriter", -> mkdir: sinon.stub().callsArg(1) unlink: sinon.stub().callsArg(1) "./ResourceListManager": @ResourceListManager = {} + "./ResourceStateManager": @ResourceStateManager = {} "wrench": @wrench = {} "./UrlCache" : @UrlCache = {} "mkdirp" : @mkdirp = sinon.stub().callsArg(1) @@ -32,8 +33,8 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceWriter.checkSyncState = sinon.stub().callsArg(2) - @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) + @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArg(2) + @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(2) @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) @ResourceListManager.loadResourceList = sinon.stub().callsArg(1) @ResourceWriter.syncResourcesToDisk({ @@ -54,7 +55,7 @@ describe "ResourceWriter", -> .should.equal true it "should store the sync state", -> - @ResourceWriter.storeSyncState + @ResourceStateManager.saveProjectStateHash .calledWith(@syncState, @basePath) .should.equal true @@ -73,8 +74,8 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceWriter.checkSyncState = sinon.stub().callsArgWith(2, null, true) - @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) + @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArg(2) + @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(2) @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) @ResourceListManager.loadResourceList = sinon.stub().callsArgWith(1, null, @resources) @ResourceWriter.syncResourcesToDisk({ @@ -85,7 +86,7 @@ describe "ResourceWriter", -> }, @basePath, @callback) it "should check the sync state matches", -> - @ResourceWriter.checkSyncState + @ResourceStateManager.checkProjectStateHashMatches .calledWith(@syncState, @basePath) .should.equal true @@ -103,6 +104,28 @@ describe "ResourceWriter", -> it "should call the callback", -> @callback.called.should.equal true + describe "syncResourcesToDisk on an incremental update when the state does not match", -> + beforeEach -> + @resources = [ + "resource-1-mock" + ] + @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArgWith(2, @error = new Error()) + @ResourceWriter.syncResourcesToDisk({ + project_id: @project_id, + syncType: "incremental", + syncState: @syncState = "1234567890abcdef", + resources: @resources + }, @basePath, @callback) + + it "should check whether the sync state matches", -> + @ResourceStateManager.checkProjectStateHashMatches + .calledWith(@syncState, @basePath) + .should.equal true + + it "should call the callback with an error", -> + @callback.calledWith(@error).should.equal true + + describe "_removeExtraneousFiles", -> beforeEach -> @output_files = [{ From fc1782e74c6a782cf1d63491b5aff719685adc03 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 18 Aug 2017 11:17:01 +0100 Subject: [PATCH 245/709] read resource files safely put a limit on the amount of data read --- app/coffee/ResourceListManager.coffee | 5 +++-- app/coffee/ResourceStateManager.coffee | 11 +++++------ app/coffee/SafeReader.coffee | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 app/coffee/SafeReader.coffee diff --git a/app/coffee/ResourceListManager.coffee b/app/coffee/ResourceListManager.coffee index d68f5d1a..aef2d2fc 100644 --- a/app/coffee/ResourceListManager.coffee +++ b/app/coffee/ResourceListManager.coffee @@ -1,8 +1,8 @@ Path = require "path" fs = require "fs" -mkdirp = require "mkdirp" logger = require "logger-sharelatex" settings = require("settings-sharelatex") +SafeReader = require "./SafeReader" module.exports = ResourceListManager = @@ -18,7 +18,8 @@ module.exports = ResourceListManager = loadResourceList: (basePath, callback = (error) ->) -> resourceListFile = Path.join(basePath, @RESOURCE_LIST_FILE) - fs.readFile resourceListFile, (err, resourceList) -> + # limit file to 128K, compile directory is user accessible + SafeReader.readFile resourceListFile, 128*1024, 'utf8', (err, resourceList) -> return callback(err) if err? resources = ({path: path} for path in resourceList?.toString()?.split("\n") or []) callback(null, resources) diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index 2a6d177b..eb193b88 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -1,9 +1,9 @@ Path = require "path" fs = require "fs" -mkdirp = require "mkdirp" logger = require "logger-sharelatex" settings = require("settings-sharelatex") Errors = require "./Errors" +SafeReader = require "./SafeReader" module.exports = ResourceStateManager = @@ -37,10 +37,9 @@ module.exports = ResourceStateManager = checkProjectStateHashMatches: (state, basePath, callback) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) - fs.readFile stateFile, {encoding:'ascii'}, (err, oldState) -> - if err? and err.code isnt 'ENOENT' - return callback(err) - else if state isnt oldState + SafeReader.readFile stateFile, 64, 'ascii', (err, oldState) -> + return callback(err) if err? + if state isnt oldState return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") - else if state is oldState + else callback(null) diff --git a/app/coffee/SafeReader.coffee b/app/coffee/SafeReader.coffee new file mode 100644 index 00000000..7ed3693c --- /dev/null +++ b/app/coffee/SafeReader.coffee @@ -0,0 +1,24 @@ +fs = require "fs" + +module.exports = SafeReader = + + # safely read up to size bytes from a file and return result as a + # string + + readFile: (file, size, encoding, callback = (error, result) ->) -> + fs.open file, 'r', (err, fd) -> + return callback() if err? and err.code is 'ENOENT' + return callback(err) if err? + + # safely return always closing the file + callbackWithClose = (err, result) -> + fs.close fd, (err1) -> + return callback(err) if err? + return callback(err1) if err1? + callback(null, result) + + buff = new Buffer(size, 0) # fill with zeros + fs.read fd, buff, 0, buff.length, 0, (err, bytesRead, buffer) -> + return callbackWithClose(err) if err? + result = buffer.toString(encoding, 0, bytesRead) + callbackWithClose(null, result) From d5b3101637aa0fbe58ab826215499f65fc2273a5 Mon Sep 17 00:00:00 2001 From: Alasdair Smith <ali@alasdairsmith.co.uk> Date: Thu, 24 Aug 2017 13:14:01 +0100 Subject: [PATCH 246/709] Update docker-runner-sharelatex config --- config/settings.defaults.coffee | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index cb7e6be7..4b391789 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -16,21 +16,16 @@ module.exports = clsiCacheDir: Path.resolve(__dirname + "/../cache") synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) - # clsi: - # strace: true - # archive_logs: true - # commandRunner: "docker-runner-sharelatex" - # docker: - # image: "quay.io/sharelatex/texlive-full" - # env: - # PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2013/bin/x86_64-linux/" - # HOME: "/tmp" - # 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` +# clsi: +# commandRunner: "docker-runner-sharelatex" +# docker: +# image: "quay.io/sharelatex/texlive-full:2017.1" +# env: +# HOME: "/tmp" +# socketPath: "/var/run/docker.sock" +# user: "tex" +# expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 +# checkProjectsIntervalMs: 10 * 60 * 1000 internal: clsi: From faa2a325cbb180b8b25ef6002b94517cfb7554ac Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 29 Aug 2017 12:09:31 +0100 Subject: [PATCH 247/709] added logging --- app/coffee/ResourceStateManager.coffee | 1 + app/coffee/ResourceWriter.coffee | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index eb193b88..946c8b56 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -39,6 +39,7 @@ module.exports = ResourceStateManager = stateFile = Path.join(basePath, @SYNC_STATE_FILE) SafeReader.readFile stateFile, 64, 'ascii', (err, oldState) -> return callback(err) if err? + logger.log state:state, oldState: oldState, basePath:basePath, stateMatches: !(state isnt oldState), "checking sync state" if state isnt oldState return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") else diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 7f670262..7c3bc732 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -16,6 +16,7 @@ module.exports = ResourceWriter = syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) -> if request.syncType is "incremental" + logger.log project_id: request.project_id, user_id: request.user_id, "incremental sync" ResourceStateManager.checkProjectStateHashMatches request.syncState, basePath, (error) -> return callback(error) if error? ResourceListManager.loadResourceList basePath, (error, resourceList) -> @@ -26,6 +27,7 @@ module.exports = ResourceWriter = return callback(error) if error? callback(null, resourceList) else + logger.log project_id: request.project_id, user_id: request.user_id, "full sync" @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? ResourceStateManager.saveProjectStateHash request.syncState, basePath, (error) -> From 3d053a2e34963ab39a9a108edfe91c7bf347fdeb Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Tue, 29 Aug 2017 14:30:43 +0100 Subject: [PATCH 248/709] Upgrade to node 6.9 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 994fe990..e18a34b9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -0.10.22 \ No newline at end of file +6.11.2 From 97d7d76e61428d4154ff31a8669f8aa83c333f4b Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 7 Sep 2017 11:54:38 +0100 Subject: [PATCH 249/709] combine the resource state and resource list to prevent them getting out of sync --- app/coffee/ResourceListManager.coffee | 25 --------------------- app/coffee/ResourceStateManager.coffee | 23 +++++++++++-------- app/coffee/ResourceWriter.coffee | 19 ++++++---------- test/unit/coffee/ResourceWriterTests.coffee | 20 +++++------------ 4 files changed, 26 insertions(+), 61 deletions(-) delete mode 100644 app/coffee/ResourceListManager.coffee diff --git a/app/coffee/ResourceListManager.coffee b/app/coffee/ResourceListManager.coffee deleted file mode 100644 index aef2d2fc..00000000 --- a/app/coffee/ResourceListManager.coffee +++ /dev/null @@ -1,25 +0,0 @@ -Path = require "path" -fs = require "fs" -logger = require "logger-sharelatex" -settings = require("settings-sharelatex") -SafeReader = require "./SafeReader" - -module.exports = ResourceListManager = - - # This file is a list of the input files for the project, one per - # line, used to identify output files (i.e. files not on this list) - # when the incoming request is incremental. - RESOURCE_LIST_FILE: ".project-resource-list" - - saveResourceList: (resources, basePath, callback = (error) ->) -> - resourceListFile = Path.join(basePath, @RESOURCE_LIST_FILE) - resourceList = (resource.path for resource in resources) - fs.writeFile resourceListFile, resourceList.join("\n"), callback - - loadResourceList: (basePath, callback = (error) ->) -> - resourceListFile = Path.join(basePath, @RESOURCE_LIST_FILE) - # limit file to 128K, compile directory is user accessible - SafeReader.readFile resourceListFile, 128*1024, 'utf8', (err, resourceList) -> - return callback(err) if err? - resources = ({path: path} for path in resourceList?.toString()?.split("\n") or []) - callback(null, resources) diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index 946c8b56..7f974050 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -11,10 +11,11 @@ module.exports = ResourceStateManager = # incremental update to be allowed. # # The initial value is passed in and stored on a full - # compile. + # compile, along with the list of resources.. # # Subsequent incremental compiles must come with the same value - if - # not they will be rejected with a 409 Conflict response. + # not they will be rejected with a 409 Conflict response. The + # previous list of resources is returned. # # An incremental compile can only update existing files with new # content. The sync state identifier must change if any docs or @@ -22,7 +23,7 @@ module.exports = ResourceStateManager = SYNC_STATE_FILE: ".project-sync-state" - saveProjectStateHash: (state, basePath, callback) -> + saveProjectStateHash: (state, resources, basePath, callback = (error) ->) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) if not state? # remove the file if no state passed in logger.log state:state, basePath:basePath, "clearing sync state" @@ -33,14 +34,18 @@ module.exports = ResourceStateManager = return callback() else logger.log state:state, basePath:basePath, "writing sync state" - fs.writeFile stateFile, state, {encoding: 'ascii'}, callback + resourceList = (resource.path for resource in resources) + fs.writeFile stateFile, [resourceList..., "stateHash:#{state}"].join("\n"), callback - checkProjectStateHashMatches: (state, basePath, callback) -> + checkProjectStateHashMatches: (state, basePath, callback = (error, resources) ->) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) - SafeReader.readFile stateFile, 64, 'ascii', (err, oldState) -> + SafeReader.readFile stateFile, 128*1024, 'utf8', (err, result) -> return callback(err) if err? - logger.log state:state, oldState: oldState, basePath:basePath, stateMatches: !(state isnt oldState), "checking sync state" - if state isnt oldState + [resourceList..., oldState] = result?.toString()?.split("\n") or [] + newState = "stateHash:#{state}" + logger.log state:state, oldState: oldState, basePath:basePath, stateMatches: (newState is oldState), "checking sync state" + if newState isnt oldState return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") else - callback(null) + resources = ({path: path} for path in resourceList) + callback(null, resources) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 7c3bc732..55f15085 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -5,7 +5,6 @@ async = require "async" mkdirp = require "mkdirp" OutputFileFinder = require "./OutputFileFinder" ResourceStateManager = require "./ResourceStateManager" -ResourceListManager = require "./ResourceListManager" Metrics = require "./Metrics" logger = require "logger-sharelatex" settings = require("settings-sharelatex") @@ -17,24 +16,20 @@ module.exports = ResourceWriter = syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) -> if request.syncType is "incremental" logger.log project_id: request.project_id, user_id: request.user_id, "incremental sync" - ResourceStateManager.checkProjectStateHashMatches request.syncState, basePath, (error) -> + ResourceStateManager.checkProjectStateHashMatches request.syncState, basePath, (error, resourceList) -> return callback(error) if error? - ResourceListManager.loadResourceList basePath, (error, resourceList) -> + ResourceWriter._removeExtraneousFiles resourceList, basePath, (error) => return callback(error) if error? - ResourceWriter._removeExtraneousFiles resourceList, basePath, (error) => + ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? - ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> - return callback(error) if error? - callback(null, resourceList) + callback(null, resourceList) else logger.log project_id: request.project_id, user_id: request.user_id, "full sync" @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? - ResourceStateManager.saveProjectStateHash request.syncState, basePath, (error) -> + ResourceStateManager.saveProjectStateHash request.syncState, request.resources, basePath, (error) -> return callback(error) if error? - ResourceListManager.saveResourceList request.resources, basePath, (error) => - return callback(error) if error? - callback(null, request.resources) + callback(null, request.resources) saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @_createDirectory basePath, (error) => @@ -81,7 +76,7 @@ module.exports = ResourceWriter = should_delete = true if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false - if path in ['.project-resource-list', '.project-sync-state'] + if path == '.project-sync-state' should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" should_delete = true diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 0804438f..520db6fd 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -10,7 +10,6 @@ describe "ResourceWriter", -> "fs": @fs = mkdir: sinon.stub().callsArg(1) unlink: sinon.stub().callsArg(1) - "./ResourceListManager": @ResourceListManager = {} "./ResourceStateManager": @ResourceStateManager = {} "wrench": @wrench = {} "./UrlCache" : @UrlCache = {} @@ -34,9 +33,7 @@ describe "ResourceWriter", -> @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArg(2) - @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(2) - @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) - @ResourceListManager.loadResourceList = sinon.stub().callsArg(1) + @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(3) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id syncState: @syncState = "0123456789abcdef" @@ -54,14 +51,9 @@ describe "ResourceWriter", -> .calledWith(@project_id, resource, @basePath) .should.equal true - it "should store the sync state", -> + it "should store the sync state and resource list", -> @ResourceStateManager.saveProjectStateHash - .calledWith(@syncState, @basePath) - .should.equal true - - it "should save the resource list", -> - @ResourceListManager.saveResourceList - .calledWith(@resources, @basePath) + .calledWith(@syncState, @resources, @basePath) .should.equal true it "should call the callback", -> @@ -74,10 +66,8 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArg(2) - @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(2) - @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) - @ResourceListManager.loadResourceList = sinon.stub().callsArgWith(1, null, @resources) + @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArgWith(2, null, @resources) + @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(3) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id, syncType: "incremental", From c1ca32184f996b6c1e1c44883f560a90dffc32a4 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 7 Sep 2017 11:55:50 +0100 Subject: [PATCH 250/709] log error if state file is truncacted --- app/coffee/SafeReader.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/coffee/SafeReader.coffee b/app/coffee/SafeReader.coffee index 7ed3693c..56ebe505 100644 --- a/app/coffee/SafeReader.coffee +++ b/app/coffee/SafeReader.coffee @@ -1,4 +1,5 @@ fs = require "fs" +logger = require "logger-sharelatex" module.exports = SafeReader = @@ -21,4 +22,6 @@ module.exports = SafeReader = fs.read fd, buff, 0, buff.length, 0, (err, bytesRead, buffer) -> return callbackWithClose(err) if err? result = buffer.toString(encoding, 0, bytesRead) + if bytesRead is size + logger.error file:file, size:size, bytesRead:bytesRead, "file truncated" callbackWithClose(null, result) From 0fac2655f769d95ec75e368f67309209f6489fae Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 7 Sep 2017 11:56:03 +0100 Subject: [PATCH 251/709] fix whitespace --- app/coffee/SafeReader.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/SafeReader.coffee b/app/coffee/SafeReader.coffee index 56ebe505..91bf8a12 100644 --- a/app/coffee/SafeReader.coffee +++ b/app/coffee/SafeReader.coffee @@ -8,7 +8,7 @@ module.exports = SafeReader = readFile: (file, size, encoding, callback = (error, result) ->) -> fs.open file, 'r', (err, fd) -> - return callback() if err? and err.code is 'ENOENT' + return callback() if err? and err.code is 'ENOENT' return callback(err) if err? # safely return always closing the file From acab9d45a002f0d5c821ed86d47f002dfcc49440 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 7 Sep 2017 13:51:36 +0100 Subject: [PATCH 252/709] log any missing files --- app/coffee/OutputFileFinder.coffee | 5 ++--- app/coffee/ResourceStateManager.coffee | 12 ++++++++++++ app/coffee/ResourceWriter.coffee | 20 ++++++++++++-------- test/unit/coffee/ResourceWriterTests.coffee | 9 +++++++-- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index c89cc8ad..80bd07f2 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -5,7 +5,7 @@ spawn = require("child_process").spawn logger = require "logger-sharelatex" module.exports = OutputFileFinder = - findOutputFiles: (resources, directory, callback = (error, outputFiles) ->) -> + findOutputFiles: (resources, directory, callback = (error, outputFiles, allFiles) ->) -> incomingResources = {} for resource in resources incomingResources[resource.path] = true @@ -16,7 +16,6 @@ module.exports = OutputFileFinder = if error? logger.err err:error, "error finding all output files" return callback(error) - jobs = [] outputFiles = [] for file in allFiles if !incomingResources[file] @@ -24,7 +23,7 @@ module.exports = OutputFileFinder = path: file type: file.match(/\.([^\.]+)$/)?[1] } - callback null, outputFiles + callback null, outputFiles, allFiles _getAllFiles: (directory, _callback = (error, fileList) ->) -> callback = (error, fileList) -> diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index 7f974050..247c9297 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -49,3 +49,15 @@ module.exports = ResourceStateManager = else resources = ({path: path} for path in resourceList) callback(null, resources) + + checkResourceFiles: (resources, allFiles, directory, callback = (error) ->) -> + # check if any of the input files are not present in list of files + seenFile = {} + for file in allFiles + seenFile[file] = true + missingFiles = (resource.path for resource in resources when not seenFile[resource.path]) + if missingFiles.length > 0 + logger.err missingFiles:missingFiles, dir:directory, allFiles:allFiles, resources:resources, "missing input files for project" + return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update") + else + callback() diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 55f15085..fa5b0101 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -18,11 +18,13 @@ module.exports = ResourceWriter = logger.log project_id: request.project_id, user_id: request.user_id, "incremental sync" ResourceStateManager.checkProjectStateHashMatches request.syncState, basePath, (error, resourceList) -> return callback(error) if error? - ResourceWriter._removeExtraneousFiles resourceList, basePath, (error) => + ResourceWriter._removeExtraneousFiles resourceList, basePath, (error, outputFiles, allFiles) -> return callback(error) if error? - ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> + ResourceStateManager.checkResourceFiles resourceList, allFiles, basePath, (error) -> return callback(error) if error? - callback(null, resourceList) + ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> + return callback(error) if error? + callback(null, resourceList) else logger.log project_id: request.project_id, user_id: request.user_id, "full sync" @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> @@ -60,13 +62,13 @@ module.exports = ResourceWriter = else return callback() - _removeExtraneousFiles: (resources, basePath, _callback = (error) ->) -> + _removeExtraneousFiles: (resources, basePath, _callback = (error, outputFiles, allFiles) ->) -> timer = new Metrics.Timer("unlink-output-files") - callback = (error) -> + callback = (error, result...) -> timer.done() - _callback(error) + _callback(error, result...) - OutputFileFinder.findOutputFiles resources, basePath, (error, outputFiles) -> + OutputFileFinder.findOutputFiles resources, basePath, (error, outputFiles, allFiles) -> return callback(error) if error? jobs = [] @@ -85,7 +87,9 @@ module.exports = ResourceWriter = if should_delete jobs.push (callback) -> ResourceWriter._deleteFileIfNotDirectory Path.join(basePath, path), callback - async.series jobs, callback + async.series jobs, (error) -> + return callback(error) if error? + callback(null, outputFiles, allFiles) _deleteFileIfNotDirectory: (path, callback = (error) ->) -> fs.stat path, (error, stat) -> diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 520db6fd..dee09523 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -32,7 +32,6 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArg(2) @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(3) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id @@ -65,9 +64,10 @@ describe "ResourceWriter", -> "resource-1-mock" ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) - @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) + @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArgWith(2, null, @outputFiles = [], @allFiles = []) @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArgWith(2, null, @resources) @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(3) + @ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id, syncType: "incremental", @@ -85,6 +85,11 @@ describe "ResourceWriter", -> .calledWith(@resources, @basePath) .should.equal true + it "should check each resource exists", -> + @ResourceStateManager.checkResourceFiles + .calledWith(@resources, @allFiles, @basePath) + .should.equal true + it "should write each resource to disk", -> for resource in @resources @ResourceWriter._writeResourceToDisk From a2c97e6f9a46cff87cb4ed92d7b65e72ad9ee669 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 8 Sep 2017 13:56:40 +0100 Subject: [PATCH 253/709] rename saveProjectStateHash to saveProjectState --- app/coffee/ResourceStateManager.coffee | 4 ++-- app/coffee/ResourceWriter.coffee | 4 ++-- test/unit/coffee/ResourceWriterTests.coffee | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index 247c9297..b894701a 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -23,7 +23,7 @@ module.exports = ResourceStateManager = SYNC_STATE_FILE: ".project-sync-state" - saveProjectStateHash: (state, resources, basePath, callback = (error) ->) -> + saveProjectState: (state, resources, basePath, callback = (error) ->) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) if not state? # remove the file if no state passed in logger.log state:state, basePath:basePath, "clearing sync state" @@ -37,7 +37,7 @@ module.exports = ResourceStateManager = resourceList = (resource.path for resource in resources) fs.writeFile stateFile, [resourceList..., "stateHash:#{state}"].join("\n"), callback - checkProjectStateHashMatches: (state, basePath, callback = (error, resources) ->) -> + checkProjectStateMatches: (state, basePath, callback = (error, resources) ->) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) SafeReader.readFile stateFile, 128*1024, 'utf8', (err, result) -> return callback(err) if err? diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index fa5b0101..f9e90b03 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -16,7 +16,7 @@ module.exports = ResourceWriter = syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) -> if request.syncType is "incremental" logger.log project_id: request.project_id, user_id: request.user_id, "incremental sync" - ResourceStateManager.checkProjectStateHashMatches request.syncState, basePath, (error, resourceList) -> + ResourceStateManager.checkProjectStateMatches request.syncState, basePath, (error, resourceList) -> return callback(error) if error? ResourceWriter._removeExtraneousFiles resourceList, basePath, (error, outputFiles, allFiles) -> return callback(error) if error? @@ -29,7 +29,7 @@ module.exports = ResourceWriter = logger.log project_id: request.project_id, user_id: request.user_id, "full sync" @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? - ResourceStateManager.saveProjectStateHash request.syncState, request.resources, basePath, (error) -> + ResourceStateManager.saveProjectState request.syncState, request.resources, basePath, (error) -> return callback(error) if error? callback(null, request.resources) diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index dee09523..fbc8916c 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -32,7 +32,7 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(3) + @ResourceStateManager.saveProjectState = sinon.stub().callsArg(3) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id syncState: @syncState = "0123456789abcdef" @@ -51,7 +51,7 @@ describe "ResourceWriter", -> .should.equal true it "should store the sync state and resource list", -> - @ResourceStateManager.saveProjectStateHash + @ResourceStateManager.saveProjectState .calledWith(@syncState, @resources, @basePath) .should.equal true @@ -65,8 +65,8 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArgWith(2, null, @outputFiles = [], @allFiles = []) - @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArgWith(2, null, @resources) - @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(3) + @ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, null, @resources) + @ResourceStateManager.saveProjectState = sinon.stub().callsArg(3) @ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id, @@ -76,7 +76,7 @@ describe "ResourceWriter", -> }, @basePath, @callback) it "should check the sync state matches", -> - @ResourceStateManager.checkProjectStateHashMatches + @ResourceStateManager.checkProjectStateMatches .calledWith(@syncState, @basePath) .should.equal true @@ -104,7 +104,7 @@ describe "ResourceWriter", -> @resources = [ "resource-1-mock" ] - @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArgWith(2, @error = new Error()) + @ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, @error = new Error()) @ResourceWriter.syncResourcesToDisk({ project_id: @project_id, syncType: "incremental", @@ -113,7 +113,7 @@ describe "ResourceWriter", -> }, @basePath, @callback) it "should check whether the sync state matches", -> - @ResourceStateManager.checkProjectStateHashMatches + @ResourceStateManager.checkProjectStateMatches .calledWith(@syncState, @basePath) .should.equal true From d04f93855b6ae9dfbdc8f20d6fd04a04d6d70f24 Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Fri, 8 Sep 2017 14:06:04 +0100 Subject: [PATCH 254/709] Add jenkinsfile (#72) * create Jenkinsfile * allow textlive image to be set with env vars * log error message in test * use sandboxed compiles variables * Add SANDBOXED_COMPILES_HOST_DIR var to test config * add SIBLING_CONTAINER_USER env var --- Jenkinsfile | 97 +++++++++++++++++++ .../coffee/ExampleDocumentTests.coffee | 4 +- test/acceptance/scripts/settings.test.coffee | 5 +- 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..97efd51f --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,97 @@ +pipeline { + + agent any + + triggers { + pollSCM('* * * * *') + cron('@daily') + } + + stages { + stage('Clean') { + steps { + // This is a terrible hack to set the file ownership to jenkins:jenkins so we can cleanup the directory + sh 'docker run -v $(pwd):/app --rm busybox /bin/chown -R 111:119 /app' + } + } + stage('Install') { + agent { + docker { + image 'node:4.2.1' + args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" + reuseNode true + } + } + steps { + sh 'git config --global core.logallrefupdates false' + sh 'rm -fr node_modules' + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: '_docker-runner'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/docker-runner-sharelatex']]]) + sh 'npm install ./_docker-runner' + sh 'rm -fr ./_docker-runner' + sh 'npm install' + sh 'npm rebuild' + sh 'npm install --quiet grunt-cli sqlite3' + } + } + stage('Compile and Test') { + agent { + docker { + image 'node:4.2.1' + reuseNode true + } + } + steps { + sh 'node_modules/.bin/grunt compile:app' + sh 'node_modules/.bin/grunt compile:acceptance_tests' + sh 'NODE_ENV=development node_modules/.bin/grunt test:unit' + } + } + stage('Acceptance Tests') { + environment { + TEXLIVE_IMAGE="quay.io/sharelatex/texlive-full:2017.1" + } + steps { + sh 'mkdir -p compiles' + // Not yet running, due to volumes/sibling containers + sh 'docker container prune -f' + sh 'docker pull $TEXLIVE_IMAGE' + sh 'docker pull sharelatex/acceptance-test-runner:clsi-4.2.1' + sh 'docker run --rm -e SIBLING_CONTAINER_USER=root -e SANDBOXED_COMPILES_HOST_DIR=$(pwd)/compiles -e SANDBOXED_COMPILES_SIBLING_CONTAINERS=true -e TEXLIVE_IMAGE=$TEXLIVE_IMAGE -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/app sharelatex/acceptance-test-runner:clsi-4.2.1' + } + } + stage('Package') { + steps { + sh 'echo ${BUILD_NUMBER} > build_number.txt' + sh 'touch build.tar.gz' // Avoid tar warning about files changing during read + sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .' + } + } + stage('Publish') { + steps { + withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { + s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") + // The deployment process uses this file to figure out the latest build + s3Upload(file:'build_number.txt', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/latest") + } + } + } + } + + post { + failure { + mail(from: "${EMAIL_ALERT_FROM}", + to: "${EMAIL_ALERT_TO}", + subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", + body: "Build: ${BUILD_URL}") + } + } + + // The options directive is for configuration that applies to the whole job. + options { + // we'd like to make sure remove old builds, so we don't fill up our storage! + buildDiscarder(logRotator(numToKeepStr:'50')) + + // And we'd really like to be sure that this build doesn't hang forever, so let's time it out after: + timeout(time: 30, unit: 'MINUTES') + } +} diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index a49f5d62..4e36c6ac 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -92,14 +92,14 @@ describe "Example Documents", -> it "should generate the correct pdf", (done) -> Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - if body?.compile?.status is "failure" + if error || body?.compile?.status is "failure" console.log "DEBUG: error", error, "body", JSON.stringify(body) pdf = Client.getOutputFile body, "pdf" downloadAndComparePdf(@project_id, example_dir, pdf.url, done) it "should generate the correct pdf on the second run as well", (done) -> Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - if body?.compile?.status is "failure" + if error || body?.compile?.status is "failure" console.log "DEBUG: error", error, "body", JSON.stringify(body) pdf = Client.getOutputFile body, "pdf" downloadAndComparePdf(@project_id, example_dir, pdf.url, done) diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.coffee index cf84c6ba..e3d3b70b 100644 --- a/test/acceptance/scripts/settings.test.coffee +++ b/test/acceptance/scripts/settings.test.coffee @@ -16,6 +16,7 @@ module.exports = clsiCacheDir: Path.resolve(__dirname + "/../../../cache") #synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) synctexBaseDir: () -> "/compile" + sandboxedCompilesHostDir: process.env['SANDBOXED_COMPILES_HOST_DIR'] clsi: #strace: true @@ -23,13 +24,13 @@ module.exports = commandRunner: "docker-runner-sharelatex" latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux docker: - image: "texlive-full:2017.1-opt" + image: process.env.TEXLIVE_IMAGE || "texlive-full:2017.1-opt" env: PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/" HOME: "/tmp" modem: socketPath: false - user: "111" + user: process.env.SIBLING_CONTAINER_USER ||"111" internal: clsi: From aa5eeb0903222116238b66219de8177422531327 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 15 Sep 2017 13:41:56 +0100 Subject: [PATCH 255/709] fallback check for missing files dot files are not examined by OutputFileFinder, so do an extra check to make sure those exist also check for any relative paths in the resources --- app/coffee/ResourceStateManager.coffee | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index b894701a..e250e644 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -4,6 +4,7 @@ logger = require "logger-sharelatex" settings = require("settings-sharelatex") Errors = require "./Errors" SafeReader = require "./SafeReader" +async = require "async" module.exports = ResourceStateManager = @@ -50,14 +51,31 @@ module.exports = ResourceStateManager = resources = ({path: path} for path in resourceList) callback(null, resources) - checkResourceFiles: (resources, allFiles, directory, callback = (error) ->) -> + checkResourceFiles: (resources, allFiles, basePath, callback = (error) ->) -> + # check the paths are all relative to current directory + for file in resources or [] + for dir in file?.path?.split('/') + if dir == '..' + return callback new Error("relative path in resource file list") # check if any of the input files are not present in list of files seenFile = {} for file in allFiles seenFile[file] = true - missingFiles = (resource.path for resource in resources when not seenFile[resource.path]) - if missingFiles.length > 0 - logger.err missingFiles:missingFiles, dir:directory, allFiles:allFiles, resources:resources, "missing input files for project" - return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update") + missingFileCandidates = (resource.path for resource in resources when not seenFile[resource.path]) + # now check if they are really missing + ResourceStateManager._checkMissingFiles missingFileCandidates, basePath, (missingFiles) -> + if missingFiles?.length > 0 + logger.err missingFiles:missingFiles, basePath:basePath, allFiles:allFiles, resources:resources, "missing input files for project" + return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update") + else + callback() + + _checkMissingFiles: (missingFileCandidates, basePath, callback = (missingFiles) ->) -> + if missingFileCandidates.length > 0 + fileDoesNotExist = (file, cb) -> + fs.stat Path.join(basePath, file), (err) -> + logger.log file:file, err:err, result: err?, "stating potential missing file" + cb(err?) + async.filterSeries missingFileCandidates, fileDoesNotExist, callback else - callback() + callback([]) From 8305268848b5316dcbc2622a4c69a2811d7d595c Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 15 Sep 2017 13:42:57 +0100 Subject: [PATCH 256/709] unit tests for ResourceStateManager --- .../coffee/ResourceStateManagerTests.coffee | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 test/unit/coffee/ResourceStateManagerTests.coffee diff --git a/test/unit/coffee/ResourceStateManagerTests.coffee b/test/unit/coffee/ResourceStateManagerTests.coffee new file mode 100644 index 00000000..6c55a1d0 --- /dev/null +++ b/test/unit/coffee/ResourceStateManagerTests.coffee @@ -0,0 +1,124 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +should = require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/ResourceStateManager' +Path = require "path" +Errors = require "../../../app/js/Errors" + +describe "ResourceStateManager", -> + beforeEach -> + @ResourceStateManager = SandboxedModule.require modulePath, requires: + "fs": @fs = {} + "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()} + "./SafeReader": @SafeReader = {} + @basePath = "/path/to/write/files/to" + @resources = [ + {path: "resource-1-mock"} + {path: "resource-2-mock"} + {path: "resource-3-mock"} + ] + @state = "1234567890" + @resourceFileName = "#{@basePath}/.project-sync-state" + @resourceFileContents = "#{@resources[0].path}\n#{@resources[1].path}\n#{@resources[2].path}\nstateHash:#{@state}" + @callback = sinon.stub() + + describe "saveProjectState", -> + beforeEach -> + @fs.writeFile = sinon.stub().callsArg(2) + + describe "when the state is specified", -> + beforeEach -> + @ResourceStateManager.saveProjectState(@state, @resources, @basePath, @callback) + + it "should write the resource list to disk", -> + @fs.writeFile + .calledWith(@resourceFileName, @resourceFileContents) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when the state is undefined", -> + beforeEach -> + @state = undefined + @fs.unlink = sinon.stub().callsArg(1) + @ResourceStateManager.saveProjectState(@state, @resources, @basePath, @callback) + + it "should unlink the resource file", -> + @fs.unlink + .calledWith(@resourceFileName) + .should.equal true + + it "should not write the resource list to disk", -> + @fs.writeFile.called.should.equal false + + it "should call the callback", -> + @callback.called.should.equal true + + describe "checkProjectStateMatches", -> + + describe "when the state matches", -> + beforeEach -> + @SafeReader.readFile = sinon.stub().callsArgWith(3, null, @resourceFileContents) + @ResourceStateManager.checkProjectStateMatches(@state, @basePath, @callback) + + it "should read the resource file", -> + @SafeReader.readFile + .calledWith(@resourceFileName) + .should.equal true + + it "should call the callback with the results", -> + @callback.calledWithMatch(null, @resources).should.equal true + + describe "when the state does not match", -> + beforeEach -> + @SafeReader.readFile = sinon.stub().callsArgWith(3, null, @resourceFileContents) + @ResourceStateManager.checkProjectStateMatches("not-the-original-state", @basePath, @callback) + + it "should call the callback with an error", -> + error = new Errors.FilesOutOfSyncError("invalid state for incremental update") + @callback.calledWith(error).should.equal true + + describe "checkResourceFiles", -> + describe "when all the files are present", -> + beforeEach -> + @allFiles = [ @resources[0].path, @resources[1].path, @resources[2].path] + @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) + + it "should call the callback", -> + @callback.calledWithExactly().should.equal true + + describe "when there is a file missing from the outputFileFinder but present on disk", -> + beforeEach -> + @allFiles = [ @resources[0].path, @resources[1].path] + @fs.stat = sinon.stub().callsArg(1) + @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) + + it "should stat the file to see if it is present", -> + @fs.stat.called.should.equal true + + it "should call the callback", -> + @callback.calledWithExactly().should.equal true + + describe "when there is a missing file", -> + beforeEach -> + @allFiles = [ @resources[0].path, @resources[1].path] + @fs.stat = sinon.stub().callsArgWith(1, new Error()) + @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) + + it "should stat the file to see if it is present", -> + @fs.stat.called.should.equal true + + it "should call the callback with an error", -> + error = new Errors.FilesOutOfSyncError("resource files missing in incremental update") + @callback.calledWith(error).should.equal true + + describe "when a resource contains a relative path", -> + beforeEach -> + @resources[0].path = "../foo/bar.tex" + @allFiles = [ @resources[0].path, @resources[1].path, @resources[2].path] + @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) + + it "should call the callback with an error", -> + @callback.calledWith(new Error("relative path in resource file list")).should.equal true + From b0f879d652ea88fdb0648db38b622be6cfd63891 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 22 Sep 2017 16:19:33 +0100 Subject: [PATCH 257/709] lock compile directory --- app/coffee/CompileController.coffee | 7 ++- app/coffee/CompileManager.coffee | 13 +++++ app/coffee/Errors.coffee | 8 +++ app/coffee/LockManager.coffee | 23 ++++++++ app/coffee/ResourceWriter.coffee | 2 +- package.json | 1 + .../unit/coffee/CompileControllerTests.coffee | 10 ++-- test/unit/coffee/CompileManagerTests.coffee | 41 ++++++++++++++ test/unit/coffee/LockManager.coffee | 54 +++++++++++++++++++ 9 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 app/coffee/LockManager.coffee create mode 100644 test/unit/coffee/LockManager.coffee diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index f4dcb0ec..60c5c64c 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -15,8 +15,11 @@ module.exports = CompileController = request.user_id = req.params.user_id if req.params.user_id? ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> return next(error) if error? - CompileManager.doCompile request, (error, outputFiles = []) -> - if error instanceof Errors.FilesOutOfSyncError + CompileManager.doCompileWithLock request, (error, outputFiles = []) -> + if error instanceof Errors.AlreadyCompilingError + code = 423 # Http 443 Locked + status = "compile-in-progress" + else if error instanceof Errors.FilesOutOfSyncError code = 409 # Http 409 Conflict status = "retry" else if error?.terminated diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 56d04db7..c4f3c7bf 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -9,6 +9,7 @@ Metrics = require "./Metrics" child_process = require "child_process" DraftModeManager = require "./DraftModeManager" TikzManager = require "./TikzManager" +LockManager = require "./LockManager" fs = require("fs") fse = require "fs-extra" os = require("os") @@ -26,6 +27,18 @@ getCompileDir = (project_id, user_id) -> Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id)) module.exports = CompileManager = + + doCompileWithLock: (request, callback = (error, outputFiles) ->) -> + compileDir = getCompileDir(request.project_id, request.user_id) + lockFile = Path.join(compileDir, ".project-lock") + # use a .project-lock file in the compile directory to prevent + # simultaneous compiles + fse.ensureDir compileDir, (error) -> + return callback(error) if error? + LockManager.runWithLock lockFile, (releaseLock) -> + CompileManager.doCompile(request, releaseLock) + , callback + doCompile: (request, callback = (error, outputFiles) ->) -> compileDir = getCompileDir(request.project_id, request.user_id) diff --git a/app/coffee/Errors.coffee b/app/coffee/Errors.coffee index 2e3ae759..b375513e 100644 --- a/app/coffee/Errors.coffee +++ b/app/coffee/Errors.coffee @@ -12,6 +12,14 @@ FilesOutOfSyncError = (message) -> return error FilesOutOfSyncError.prototype.__proto__ = Error.prototype +AlreadyCompilingError = (message) -> + error = new Error(message) + error.name = "AlreadyCompilingError" + error.__proto__ = AlreadyCompilingError.prototype + return error +AlreadyCompilingError.prototype.__proto__ = Error.prototype + module.exports = Errors = NotFoundError: NotFoundError FilesOutOfSyncError: FilesOutOfSyncError + AlreadyCompilingError: AlreadyCompilingError diff --git a/app/coffee/LockManager.coffee b/app/coffee/LockManager.coffee new file mode 100644 index 00000000..5d6fa462 --- /dev/null +++ b/app/coffee/LockManager.coffee @@ -0,0 +1,23 @@ +Settings = require('settings-sharelatex') +logger = require "logger-sharelatex" +Lockfile = require('lockfile') # from https://github.com/npm/lockfile +Errors = require "./Errors" + +module.exports = LockManager = + LOCK_TEST_INTERVAL: 1000 # 50ms between each test of the lock + MAX_LOCK_WAIT_TIME: 15000 # 10s maximum time to spend trying to get the lock + LOCK_STALE: 5*60*1000 # 5 mins time until lock auto expires + + runWithLock: (path, runner = ((releaseLock = (error) ->) ->), callback = ((error) ->)) -> + lockOpts = + wait: @MAX_LOCK_WAIT_TIME + pollPeriod: @LOCK_TEST_INTERVAL + stale: @LOCK_STALE + Lockfile.lock path, lockOpts, (error) -> + return callback new Errors.AlreadyCompilingError("compile in progress") if error?.code is 'EEXIST' + return callback(error) if error? + runner (error1, args...) -> + Lockfile.unlock path, (error2) -> + error = error1 or error2 + return callback(error) if error? + callback(null, args...) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index f9e90b03..06d5692f 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -78,7 +78,7 @@ module.exports = ResourceWriter = should_delete = true if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false - if path == '.project-sync-state' + if path == '.project-sync-state' or path == '.project-lock' should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" should_delete = true diff --git a/package.json b/package.json index 56843026..71711be1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "fs-extra": "^0.16.3", "grunt-mkdir": "^1.0.0", "heapdump": "^0.3.5", + "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index 1fc6a99b..7b6001d0 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -49,7 +49,7 @@ describe "CompileController", -> describe "successfully", -> beforeEach -> - @CompileManager.doCompile = sinon.stub().callsArgWith(1, null, @output_files) + @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, @output_files) @CompileController.compile @req, @res it "should parse the request", -> @@ -58,7 +58,7 @@ describe "CompileController", -> .should.equal true it "should run the compile for the specified project", -> - @CompileManager.doCompile + @CompileManager.doCompileWithLock .calledWith(@request_with_project_id) .should.equal true @@ -84,7 +84,7 @@ describe "CompileController", -> describe "with an error", -> beforeEach -> - @CompileManager.doCompile = sinon.stub().callsArgWith(1, new Error(@message = "error message"), null) + @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, new Error(@message = "error message"), null) @CompileController.compile @req, @res it "should return the JSON response with the error", -> @@ -102,7 +102,7 @@ describe "CompileController", -> beforeEach -> @error = new Error(@message = "container timed out") @error.timedout = true - @CompileManager.doCompile = sinon.stub().callsArgWith(1, @error, null) + @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, @error, null) @CompileController.compile @req, @res it "should return the JSON response with the timeout status", -> @@ -118,7 +118,7 @@ describe "CompileController", -> describe "when the request returns no output files", -> beforeEach -> - @CompileManager.doCompile = sinon.stub().callsArgWith(1, null, []) + @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, []) @CompileController.compile @req, @res it "should return the JSON response with the failure status", -> diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index ff671b27..b07a02cd 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -19,9 +19,50 @@ describe "CompileManager", -> "./CommandRunner": @CommandRunner = {} "./DraftModeManager": @DraftModeManager = {} "./TikzManager": @TikzManager = {} + "./LockManager": @LockManager = {} "fs": @fs = {} @callback = sinon.stub() + describe "doCompileWithLock", -> + beforeEach -> + @request = + resources: @resources = "mock-resources" + project_id: @project_id = "project-id-123" + user_id: @user_id = "1234" + @output_files = ["foo", "bar"] + @CompileManager.doCompile = sinon.stub().callsArgWith(1, null, @output_files) + @LockManager.runWithLock = (lockFile, runner, callback) -> + runner (err, result...) -> + callback(err, result...) + + describe "when the project is not locked", -> + beforeEach -> + @CompileManager.doCompileWithLock @request, @callback + + it "should call doCompile with the request", -> + @CompileManager.doCompile + .calledWith(@request) + .should.equal true + + it "should call the callback with the output files", -> + @callback.calledWithExactly(null, @output_files) + .should.equal true + + describe "when the project is locked", -> + beforeEach -> + @error = new Error("locked") + @LockManager.runWithLock = (lockFile, runner, callback) => + callback(@error) + @CompileManager.doCompileWithLock @request, @callback + + it "should not call doCompile with the request", -> + @CompileManager.doCompile + .called.should.equal false + + it "should call the callback with the error", -> + @callback.calledWithExactly(@error) + .should.equal true + describe "doCompile", -> beforeEach -> @output_files = [{ diff --git a/test/unit/coffee/LockManager.coffee b/test/unit/coffee/LockManager.coffee new file mode 100644 index 00000000..c1071a58 --- /dev/null +++ b/test/unit/coffee/LockManager.coffee @@ -0,0 +1,54 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/LockManager' +Path = require "path" +Errors = require "../../../app/js/Errors" + +describe "LockManager", -> + beforeEach -> + @LockManager = SandboxedModule.require modulePath, requires: + "settings-sharelatex": {} + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + "lockfile": @Lockfile = {} + @lockFile = "/local/compile/directory/.project-lock" + + describe "runWithLock", -> + beforeEach -> + @runner = sinon.stub().callsArgWith(0, null, "foo", "bar") + @callback = sinon.stub() + + describe "normally", -> + beforeEach -> + @Lockfile.lock = sinon.stub().callsArgWith(2, null) + @Lockfile.unlock = sinon.stub().callsArgWith(1, null) + @LockManager.runWithLock @lockFile, @runner, @callback + + it "should run the compile", -> + @runner + .calledWith() + .should.equal true + + it "should call the callback with the response from the compile", -> + @callback + .calledWithExactly(null, "foo", "bar") + .should.equal true + + describe "when the project is locked", -> + beforeEach -> + @error = new Error() + @error.code = "EEXIST" + @Lockfile.lock = sinon.stub().callsArgWith(2,@error) + @Lockfile.unlock = sinon.stub().callsArgWith(1, null) + @LockManager.runWithLock @lockFile, @runner, @callback + + it "should not run the compile", -> + @runner + .called + .should.equal false + + it "should return an error", -> + error = new Errors.AlreadyCompilingError() + @callback + .calledWithExactly(error) + .should.equal true From eaa99c7274fdd9fa6beb467ddaf921eaa0465594 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 25 Sep 2017 15:28:31 +0100 Subject: [PATCH 258/709] fix unit tests for use of fs-extra --- test/unit/coffee/CompileManagerTests.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index b07a02cd..591939d6 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -21,6 +21,7 @@ describe "CompileManager", -> "./TikzManager": @TikzManager = {} "./LockManager": @LockManager = {} "fs": @fs = {} + "fs-extra": @fse = { ensureDir: sinon.stub().callsArg(1) } @callback = sinon.stub() describe "doCompileWithLock", -> @@ -30,6 +31,8 @@ describe "CompileManager", -> project_id: @project_id = "project-id-123" user_id: @user_id = "1234" @output_files = ["foo", "bar"] + @Settings.compileDir = "compiles" + @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @CompileManager.doCompile = sinon.stub().callsArgWith(1, null, @output_files) @LockManager.runWithLock = (lockFile, runner, callback) -> runner (err, result...) -> @@ -39,6 +42,10 @@ describe "CompileManager", -> beforeEach -> @CompileManager.doCompileWithLock @request, @callback + it "should ensure that the compile directory exists", -> + @fse.ensureDir.calledWith(@compileDir) + .should.equal true + it "should call doCompile with the request", -> @CompileManager.doCompile .calledWith(@request) @@ -55,6 +62,10 @@ describe "CompileManager", -> callback(@error) @CompileManager.doCompileWithLock @request, @callback + it "should ensure that the compile directory exists", -> + @fse.ensureDir.calledWith(@compileDir) + .should.equal true + it "should not call doCompile with the request", -> @CompileManager.doCompile .called.should.equal false From a36ec7f54eef7e6f30c68957b16f513146be9efe Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 25 Sep 2017 16:06:45 +0100 Subject: [PATCH 259/709] fix comment --- app/coffee/CompileController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 60c5c64c..99973fdd 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -17,7 +17,7 @@ module.exports = CompileController = return next(error) if error? CompileManager.doCompileWithLock request, (error, outputFiles = []) -> if error instanceof Errors.AlreadyCompilingError - code = 423 # Http 443 Locked + code = 423 # Http 423 Locked status = "compile-in-progress" else if error instanceof Errors.FilesOutOfSyncError code = 409 # Http 409 Conflict From 0930b1cd8f4f2a8c3dca70fe406c419746a45648 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 26 Sep 2017 09:47:29 +0100 Subject: [PATCH 260/709] only exclude clsi-specific files from output list --- app/coffee/OutputFileFinder.coffee | 6 ++++-- app/coffee/ResourceWriter.coffee | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index 80bd07f2..4b07f6e1 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -29,8 +29,10 @@ module.exports = OutputFileFinder = callback = (error, fileList) -> _callback(error, fileList) _callback = () -> - - args = [directory, "-name", ".*", "-prune", "-o", "-type", "f", "-print"] + + # don't include clsi-specific files/directories in the output list + EXCLUDE_DIRS = ["-name", ".cache", "-o", "-name", ".archive","-o", "-name", ".project-*"] + args = [directory, "(", EXCLUDE_DIRS..., ")", "-prune", "-o", "-type", "f", "-print"] logger.log args: args, "running find command" proc = spawn("find", args) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index f9e90b03..55970ee8 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -78,8 +78,6 @@ module.exports = ResourceWriter = should_delete = true if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false - if path == '.project-sync-state' - should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" should_delete = true if path == "output.tex" # created by TikzManager if present in output files From f11468b5955dd16901954c5fff1591a564e4b636 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 26 Sep 2017 09:48:09 +0100 Subject: [PATCH 261/709] remove stat test for missing files --- app/coffee/ResourceStateManager.coffee | 23 ++++--------------- .../coffee/ResourceStateManagerTests.coffee | 15 ------------ 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index e250e644..fbd4c677 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -4,7 +4,6 @@ logger = require "logger-sharelatex" settings = require("settings-sharelatex") Errors = require "./Errors" SafeReader = require "./SafeReader" -async = require "async" module.exports = ResourceStateManager = @@ -61,21 +60,9 @@ module.exports = ResourceStateManager = seenFile = {} for file in allFiles seenFile[file] = true - missingFileCandidates = (resource.path for resource in resources when not seenFile[resource.path]) - # now check if they are really missing - ResourceStateManager._checkMissingFiles missingFileCandidates, basePath, (missingFiles) -> - if missingFiles?.length > 0 - logger.err missingFiles:missingFiles, basePath:basePath, allFiles:allFiles, resources:resources, "missing input files for project" - return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update") - else - callback() - - _checkMissingFiles: (missingFileCandidates, basePath, callback = (missingFiles) ->) -> - if missingFileCandidates.length > 0 - fileDoesNotExist = (file, cb) -> - fs.stat Path.join(basePath, file), (err) -> - logger.log file:file, err:err, result: err?, "stating potential missing file" - cb(err?) - async.filterSeries missingFileCandidates, fileDoesNotExist, callback + missingFiles = (resource.path for resource in resources when not seenFile[resource.path]) + if missingFiles?.length > 0 + logger.err missingFiles:missingFiles, basePath:basePath, allFiles:allFiles, resources:resources, "missing input files for project" + return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update") else - callback([]) + callback() diff --git a/test/unit/coffee/ResourceStateManagerTests.coffee b/test/unit/coffee/ResourceStateManagerTests.coffee index 6c55a1d0..e5e1c130 100644 --- a/test/unit/coffee/ResourceStateManagerTests.coffee +++ b/test/unit/coffee/ResourceStateManagerTests.coffee @@ -88,27 +88,12 @@ describe "ResourceStateManager", -> it "should call the callback", -> @callback.calledWithExactly().should.equal true - describe "when there is a file missing from the outputFileFinder but present on disk", -> - beforeEach -> - @allFiles = [ @resources[0].path, @resources[1].path] - @fs.stat = sinon.stub().callsArg(1) - @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) - - it "should stat the file to see if it is present", -> - @fs.stat.called.should.equal true - - it "should call the callback", -> - @callback.calledWithExactly().should.equal true - describe "when there is a missing file", -> beforeEach -> @allFiles = [ @resources[0].path, @resources[1].path] @fs.stat = sinon.stub().callsArgWith(1, new Error()) @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) - it "should stat the file to see if it is present", -> - @fs.stat.called.should.equal true - it "should call the callback with an error", -> error = new Errors.FilesOutOfSyncError("resource files missing in incremental update") @callback.calledWith(error).should.equal true From dbeff9a7b8b960801d3dd3b575fbca63df3a74de Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 26 Sep 2017 10:42:59 +0100 Subject: [PATCH 262/709] exclude hidden files from output express static server doesn't serve them and rejects with 404 --- app/coffee/OutputCacheManager.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 465b0431..41786af9 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -63,6 +63,11 @@ module.exports = OutputCacheManager = # copy all the output files into the new cache directory results = [] async.mapSeries outputFiles, (file, cb) -> + # don't send dot files as output, express doesn't serve them + if file?.path?.match(/^\.|\/./) + logger.warn compileDir: compileDir, path: file.path, "ignoring dotfile in output" + return cb() + # copy other files into cache directory if valid newFile = _.clone(file) [src, dst] = [Path.join(compileDir, file.path), Path.join(cacheDir, file.path)] OutputCacheManager._checkFileIsSafe src, (err, isSafe) -> From 23fec681113682e339b52b6bebb8729166778893 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 26 Sep 2017 11:03:20 +0100 Subject: [PATCH 263/709] use a separate function for hidden file check --- app/coffee/OutputCacheManager.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 41786af9..23e179c4 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -64,7 +64,7 @@ module.exports = OutputCacheManager = results = [] async.mapSeries outputFiles, (file, cb) -> # don't send dot files as output, express doesn't serve them - if file?.path?.match(/^\.|\/./) + if OutputCacheManager._fileIsHidden(file.path) logger.warn compileDir: compileDir, path: file.path, "ignoring dotfile in output" return cb() # copy other files into cache directory if valid @@ -149,6 +149,9 @@ module.exports = OutputCacheManager = removeDir dir, cb , callback + _fileIsHidden: (path) -> + return path?.match(/^\.|\/./)? + _checkFileIsSafe: (src, callback = (error, isSafe) ->) -> # check if we have a valid file to copy into the cache fs.stat src, (err, stats) -> From c3e3e3d8ac9b43539e0019e26f3cf0e73fa14f45 Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Tue, 26 Sep 2017 11:44:48 +0100 Subject: [PATCH 264/709] Update Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 97efd51f..9360d1f3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -30,7 +30,7 @@ pipeline { sh 'rm -fr ./_docker-runner' sh 'npm install' sh 'npm rebuild' - sh 'npm install --quiet grunt-cli sqlite3' + sh 'npm install --quiet grunt-cli' } } stage('Compile and Test') { From bd5a0ef36f5623dccb2422b0196f4aff3bd931ec Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Thu, 28 Sep 2017 11:50:33 +0100 Subject: [PATCH 265/709] Jg jenkinsfile cleanup (#75) * Update Jenkinsfile make sure we don't ship unneeded build files * Update ExampleDocumentTests.coffee * use node 6.11.2 in jenkins file --- Jenkinsfile | 19 +++++++++++-------- .../coffee/ExampleDocumentTests.coffee | 3 +++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9360d1f3..e713fa73 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,13 +12,14 @@ pipeline { steps { // This is a terrible hack to set the file ownership to jenkins:jenkins so we can cleanup the directory sh 'docker run -v $(pwd):/app --rm busybox /bin/chown -R 111:119 /app' + sh 'rm -fr node_modules' } } stage('Install') { agent { docker { - image 'node:4.2.1' - args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" + image 'node:6.11.2' + args "-e HOME=/tmp" reuseNode true } } @@ -27,16 +28,15 @@ pipeline { sh 'rm -fr node_modules' checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: '_docker-runner'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/docker-runner-sharelatex']]]) sh 'npm install ./_docker-runner' - sh 'rm -fr ./_docker-runner' + sh 'rm -fr ./_docker-runner ./_docker-runner@tmp' sh 'npm install' - sh 'npm rebuild' sh 'npm install --quiet grunt-cli' } } stage('Compile and Test') { agent { docker { - image 'node:4.2.1' + image 'node:6.11.2' reuseNode true } } @@ -51,12 +51,15 @@ pipeline { TEXLIVE_IMAGE="quay.io/sharelatex/texlive-full:2017.1" } steps { - sh 'mkdir -p compiles' + sh 'mkdir -p compiles cache' // Not yet running, due to volumes/sibling containers sh 'docker container prune -f' sh 'docker pull $TEXLIVE_IMAGE' - sh 'docker pull sharelatex/acceptance-test-runner:clsi-4.2.1' - sh 'docker run --rm -e SIBLING_CONTAINER_USER=root -e SANDBOXED_COMPILES_HOST_DIR=$(pwd)/compiles -e SANDBOXED_COMPILES_SIBLING_CONTAINERS=true -e TEXLIVE_IMAGE=$TEXLIVE_IMAGE -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/app sharelatex/acceptance-test-runner:clsi-4.2.1' + sh 'docker pull sharelatex/acceptance-test-runner:clsi-6.11.2' + sh 'docker run --rm -e SIBLING_CONTAINER_USER=root -e SANDBOXED_COMPILES_HOST_DIR=$(pwd)/compiles -e SANDBOXED_COMPILES_SIBLING_CONTAINERS=true -e TEXLIVE_IMAGE=$TEXLIVE_IMAGE -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/app sharelatex/acceptance-test-runner:clsi-6.11.2' + // This is a terrible hack to set the file ownership to jenkins:jenkins so we can cleanup the directory + sh 'docker run -v $(pwd):/app --rm busybox /bin/chown -R 111:119 /app' + sh 'rm -r compiles cache server.log db.sqlite config/settings.defaults.coffee' } } stage('Package') { diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 4e36c6ac..4d431c29 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -12,6 +12,9 @@ catch e convertToPng = (pdfPath, pngPath, callback = (error) ->) -> convert = ChildProcess.exec "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" + stdout = "" + convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() + convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() convert.on "exit", () -> callback() From fd0cbb2c5299da8677f99eb8fe3e4cd299a9058f Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Thu, 28 Sep 2017 11:51:41 +0100 Subject: [PATCH 266/709] use npm cache in CI build --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index e713fa73..47557483 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,7 +19,7 @@ pipeline { agent { docker { image 'node:6.11.2' - args "-e HOME=/tmp" + args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" reuseNode true } } From 88eafdf5754b491de94f1bd9c0700e93a22f3fd7 Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Thu, 28 Sep 2017 13:46:01 +0100 Subject: [PATCH 267/709] Update Jenkinsfile --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 47557483..0c289e15 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -30,6 +30,7 @@ pipeline { sh 'npm install ./_docker-runner' sh 'rm -fr ./_docker-runner ./_docker-runner@tmp' sh 'npm install' + sh 'npm rebuild' sh 'npm install --quiet grunt-cli' } } From d1aa1d84fb21d738aa378d1ade64bc8790b651f5 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Thu, 28 Sep 2017 16:36:25 +0100 Subject: [PATCH 268/709] keep tikzexternalize files --- app/coffee/ResourceWriter.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 55970ee8..0b6aef5b 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -78,6 +78,8 @@ module.exports = ResourceWriter = should_delete = true if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false + if path.match(/^output-.*/) # Tikz cached figures + should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" should_delete = true if path == "output.tex" # created by TikzManager if present in output files From 1da918e13c2b24144ecd83fbfc33c94ca80c558f Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 29 Sep 2017 17:00:53 +0100 Subject: [PATCH 269/709] simplify tikzexternalize checks --- app/coffee/CompileManager.coffee | 10 +-- app/coffee/TikzManager.coffee | 27 ++++--- test/unit/coffee/CompileManagerTests.coffee | 2 +- test/unit/coffee/TikzManager.coffee | 79 ++++++++++++++------- 4 files changed, 73 insertions(+), 45 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index c4f3c7bf..167a80e1 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -62,10 +62,12 @@ module.exports = CompileManager = callback() createTikzFileIfRequired = (callback) -> - if TikzManager.needsOutputFile(request.rootResourcePath, resourceList) - TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback - else - callback() + TikzManager.checkMainFile compileDir, request.rootResourcePath, resourceList, (error, usesTikzExternalize) -> + return callback(error) if error? + if usesTikzExternalize + TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback + else + callback() # set up environment variables for chktex env = {} diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index 5c08f205..07f87e3c 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -1,6 +1,7 @@ fs = require "fs" Path = require "path" ResourceWriter = require "./ResourceWriter" +SafeReader = require "./SafeReader" logger = require "logger-sharelatex" # for \tikzexternalize to work the main file needs to match the @@ -8,25 +9,21 @@ logger = require "logger-sharelatex" # copy of the main file as 'output.tex'. module.exports = TikzManager = - needsOutputFile: (rootResourcePath, resources) -> + + checkMainFile: (compileDir, mainFile, resources, callback = (error, usesTikzExternalize) ->) -> # if there's already an output.tex file, we don't want to touch it for resource in resources if resource.path is "output.tex" - return false + logger.log compileDir: compileDir, mainFile: mainFile, "output.tex already in resources" + return callback(null, false) # if there's no output.tex, see if we are using tikz/pgf in the main file - for resource in resources - if resource.path is rootResourcePath - return TikzManager._includesTikz (resource) - # otherwise false - return false - - _includesTikz: (resource) -> - # check if we are using tikz externalize - content = resource.content?.slice(0,65536) - if content?.indexOf("\\tikzexternalize") >= 0 - return true - else - return false + ResourceWriter.checkPath compileDir, mainFile, (error, path) -> + return callback(error) if error? + SafeReader.readFile path, 65536, "utf8", (error, content) -> + return callback(error) if error? + usesTikzExternalize = content?.indexOf("\\tikzexternalize") >= 0 + logger.log compileDir: compileDir, mainFile: mainFile, usesTikzExternalize:usesTikzExternalize, "checked for tikzexternalize" + callback null, usesTikzExternalize injectOutputFile: (compileDir, mainFile, callback = (error) ->) -> ResourceWriter.checkPath compileDir, mainFile, (error, path) -> diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 591939d6..341ce2d0 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -108,7 +108,7 @@ describe "CompileManager", -> @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) @DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) - @TikzManager.needsOutputFile = sinon.stub().returns(false) + @TikzManager.checkMainFile = sinon.stub().callsArg(3, false) describe "normally", -> beforeEach -> diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.coffee index 816b3b10..5a3ec5ce 100644 --- a/test/unit/coffee/TikzManager.coffee +++ b/test/unit/coffee/TikzManager.coffee @@ -7,34 +7,63 @@ describe 'TikzManager', -> beforeEach -> @TikzManager = SandboxedModule.require modulePath, requires: "./ResourceWriter": @ResourceWriter = {} + "./SafeReader": @SafeReader = {} "fs": @fs = {} "logger-sharelatex": @logger = {log: () ->} - describe "needsOutputFile", -> - it "should return true if there is a \\tikzexternalize", -> - @TikzManager.needsOutputFile("main.tex", [ - { path: 'foo.tex' }, - { path: 'main.tex', content:'foo \\usepackage{tikz} \\tikzexternalize' } - ]).should.equal true - - it "should return false if there is no \\tikzexternalize", -> - @TikzManager.needsOutputFile("main.tex", [ - { path: 'foo.tex' }, - { path: 'main.tex', content:'foo \\usepackage{tikz}' } - ]).should.equal false - - it "should return false if there is already an output.tex file", -> - @TikzManager.needsOutputFile("main.tex", [ - { path: 'foo.tex' }, - { path: 'main.tex', content:'foo \\usepackage{tikz} \\tikzexternalize' }, - { path: 'output.tex' } - ]).should.equal false - - it "should return false if the file has no content", -> - @TikzManager.needsOutputFile("main.tex", [ - { path: 'foo.tex' }, - { path: 'main.tex' } - ]).should.equal false + describe "checkMainFile", -> + beforeEach -> + @compileDir = "compile-dir" + @mainFile = "main.tex" + @callback = sinon.stub() + + describe "if there is already an output.tex file in the resources", -> + beforeEach -> + @resources = [{path:"main.tex"},{path:"output.tex"}] + @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback + + it "should call the callback with false ", -> + @callback.calledWithExactly(null, false) + .should.equal true + + describe "if there is no output.tex file in the resources", -> + beforeEach -> + @resources = [{path:"main.tex"}] + @ResourceWriter.checkPath = sinon.stub() + .withArgs(@compileDir, @mainFile) + .callsArgWith(2, null, "#{@compileDir}/#{@mainFile}") + + describe "and the main file contains tikzexternalize", -> + beforeEach -> + @SafeReader.readFile = sinon.stub() + .withArgs("#{@compileDir}/#{@mainFile}") + .callsArgWith(3, null, "hello \\tikzexternalize") + @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback + + it "should look at the file on disk", -> + @SafeReader.readFile + .calledWith("#{@compileDir}/#{@mainFile}") + .should.equal true + + it "should call the callback with true ", -> + @callback.calledWithExactly(null, true) + .should.equal true + + describe "and the main file does not contain tikzexternalize", -> + beforeEach -> + @SafeReader.readFile = sinon.stub() + .withArgs("#{@compileDir}/#{@mainFile}") + .callsArgWith(3, null, "hello") + @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback + + it "should look at the file on disk", -> + @SafeReader.readFile + .calledWith("#{@compileDir}/#{@mainFile}") + .should.equal true + + it "should call the callback with false", -> + @callback.calledWithExactly(null, false) + .should.equal true describe "injectOutputFile", -> beforeEach -> From 60ad4252054e9baeb8ed2778d4ad79cfa31de104 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 2 Oct 2017 15:44:00 +0100 Subject: [PATCH 270/709] move logging from SafeReader into caller prevent unnecessary logging when looking at headers of files where hitting the end of the file is expected. --- app/coffee/ResourceStateManager.coffee | 6 +++++- app/coffee/SafeReader.coffee | 8 +++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.coffee index fbd4c677..19fea47d 100644 --- a/app/coffee/ResourceStateManager.coffee +++ b/app/coffee/ResourceStateManager.coffee @@ -22,6 +22,7 @@ module.exports = ResourceStateManager = # files are moved, added, deleted or renamed. SYNC_STATE_FILE: ".project-sync-state" + SYNC_STATE_MAX_SIZE: 128*1024 saveProjectState: (state, resources, basePath, callback = (error) ->) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) @@ -39,8 +40,11 @@ module.exports = ResourceStateManager = checkProjectStateMatches: (state, basePath, callback = (error, resources) ->) -> stateFile = Path.join(basePath, @SYNC_STATE_FILE) - SafeReader.readFile stateFile, 128*1024, 'utf8', (err, result) -> + size = @SYNC_STATE_MAX_SIZE + SafeReader.readFile stateFile, size, 'utf8', (err, result, bytesRead) -> return callback(err) if err? + if bytesRead is size + logger.error file:stateFile, size:size, bytesRead:bytesRead, "project state file truncated" [resourceList..., oldState] = result?.toString()?.split("\n") or [] newState = "stateHash:#{state}" logger.log state:state, oldState: oldState, basePath:basePath, stateMatches: (newState is oldState), "checking sync state" diff --git a/app/coffee/SafeReader.coffee b/app/coffee/SafeReader.coffee index 91bf8a12..adb96b16 100644 --- a/app/coffee/SafeReader.coffee +++ b/app/coffee/SafeReader.coffee @@ -12,16 +12,14 @@ module.exports = SafeReader = return callback(err) if err? # safely return always closing the file - callbackWithClose = (err, result) -> + callbackWithClose = (err, result...) -> fs.close fd, (err1) -> return callback(err) if err? return callback(err1) if err1? - callback(null, result) + callback(null, result...) buff = new Buffer(size, 0) # fill with zeros fs.read fd, buff, 0, buff.length, 0, (err, bytesRead, buffer) -> return callbackWithClose(err) if err? result = buffer.toString(encoding, 0, bytesRead) - if bytesRead is size - logger.error file:file, size:size, bytesRead:bytesRead, "file truncated" - callbackWithClose(null, result) + callbackWithClose(null, result, bytesRead) From 86cc30d8fad13c918ae3903f020a4bc4544fdd26 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 2 Oct 2017 15:45:09 +0100 Subject: [PATCH 271/709] fix typo in log message --- app/coffee/TikzManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index 07f87e3c..7605b0d2 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -30,6 +30,6 @@ module.exports = TikzManager = return callback(error) if error? fs.readFile path, "utf8", (error, content) -> return callback(error) if error? - logger.log compileDir: compileDir, mainFile: mainFile, "copied file to ouput.tex for tikz" + logger.log compileDir: compileDir, mainFile: mainFile, "copied file to output.tex for tikz" # use wx flag to ensure that output file does not already exist fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback From eb35cab72de4d06eb40ccfa8a8c9a75db1816afa Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Thu, 12 Oct 2017 16:54:54 +0100 Subject: [PATCH 272/709] only alert on master --- Jenkinsfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 0c289e15..fb859d72 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -83,6 +83,10 @@ pipeline { post { failure { + when { + branch 'master' + } + mail(from: "${EMAIL_ALERT_FROM}", to: "${EMAIL_ALERT_TO}", subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", From 23f4f2175c2943a46d9e35ac5fdf67e56813ce17 Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Mon, 16 Oct 2017 14:13:51 +0100 Subject: [PATCH 273/709] Update Jenkinsfile --- Jenkinsfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index fb859d72..0c289e15 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -83,10 +83,6 @@ pipeline { post { failure { - when { - branch 'master' - } - mail(from: "${EMAIL_ALERT_FROM}", to: "${EMAIL_ALERT_TO}", subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", From 360e8220ce324301e957680a1c2e8811070b8c06 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Fri, 20 Oct 2017 15:16:35 +0100 Subject: [PATCH 274/709] exit if mock server fails to start --- test/acceptance/coffee/helpers/Client.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/acceptance/coffee/helpers/Client.coffee b/test/acceptance/coffee/helpers/Client.coffee index 3e1a5b86..c67b4251 100644 --- a/test/acceptance/coffee/helpers/Client.coffee +++ b/test/acceptance/coffee/helpers/Client.coffee @@ -30,7 +30,10 @@ module.exports = Client = express = require("express") app = express() app.use express.static(directory) - app.listen(port, host) + app.listen(port, host).on "error", (error) -> + console.error "error starting server:", error.message + process.exit(1) + syncFromCode: (project_id, file, line, column, callback = (error, pdfPositions) ->) -> request.get { From 3692570df02c69fc31b78b4a5edb741a1e35f465 Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Wed, 29 Nov 2017 11:01:51 +0000 Subject: [PATCH 275/709] Increase smoke test interval to 30 seconds The smoke tests can sometimes take ~20 seconds to complete, which causes the http POST to time out. This should solve that problem. --- app.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index b99b2774..5c79b8ee 100644 --- a/app.coffee +++ b/app.coffee @@ -133,7 +133,7 @@ if Settings.smokeTest do runSmokeTest = -> logger.log("running smoke tests") smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher) - setTimeout(runSmokeTest, 20 * 1000) + setTimeout(runSmokeTest, 30 * 1000) app.get "/health_check", (req, res)-> res.contentType(resCacher?.setContentType) From 6d42e18088b45526d3342e4ee718f11c41a004c1 Mon Sep 17 00:00:00 2001 From: Joe Green <joe.green0991@gmail.com> Date: Tue, 5 Dec 2017 16:51:59 +0000 Subject: [PATCH 276/709] Add a 1 second delay to the smoke tests (#81) * Add a 1 second delay to the smoke tests Fixes a race condition where smoke tests exit before container can be attached to. See here for more info: https://github.com/overleaf/sharelatex/issues/274 * give the smoke tests additional work to do * escape slashes --- test/smoke/coffee/SmokeTests.coffee | 35 +++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/test/smoke/coffee/SmokeTests.coffee b/test/smoke/coffee/SmokeTests.coffee index 372ca69d..9ecf09c1 100644 --- a/test/smoke/coffee/SmokeTests.coffee +++ b/test/smoke/coffee/SmokeTests.coffee @@ -17,10 +17,37 @@ describe "Running a compile", -> resources: [ path: "main.tex" content: """ - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} +% Membrane-like surface +% Author: Yotam Avital +\\documentclass{article} +\\usepackage{tikz} +\\usetikzlibrary{calc,fadings,decorations.pathreplacing} +\\begin{document} +\\begin{tikzpicture} + \\def\\nuPi{3.1459265} + \\foreach \\i in {5,4,...,2}{% This one doesn't matter + \\foreach \\j in {3,2,...,0}{% This will crate a membrane + % with the front lipids visible + % top layer + \\pgfmathsetmacro{\\dx}{rand*0.1}% A random variance in the x coordinate + \\pgfmathsetmacro{\\dy}{rand*0.1}% A random variance in the y coordinate, + % gives a hight fill to the lipid + \\pgfmathsetmacro{\\rot}{rand*0.1}% A random variance in the + % molecule orientation + \\shade[ball color=red] ({\\i+\\dx+\\rot},{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)}) circle(0.45); + \\shade[ball color=gray] (\\i+\\dx,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-0.9}) circle(0.45); + \\shade[ball color=gray] (\\i+\\dx-\\rot,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-1.8}) circle(0.45); + % bottom layer + \\pgfmathsetmacro{\\dx}{rand*0.1} + \\pgfmathsetmacro{\\dy}{rand*0.1} + \\pgfmathsetmacro{\\rot}{rand*0.1} + \\shade[ball color=gray] (\\i+\\dx+\\rot,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-2.8}) circle(0.45); + \\shade[ball color=gray] (\\i+\\dx,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-3.7}) circle(0.45); + \\shade[ball color=red] (\\i+\\dx-\\rot,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-4.6}) circle(0.45); + } + } +\\end{tikzpicture} +\\end{document} """ ] }, (@error, @response, @body) => From f58ef67875509867d3b4a5be7871fd884ffb4bc1 Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Fri, 29 Dec 2017 08:08:19 +0000 Subject: [PATCH 277/709] Provide hosts and siblings container as environment settings and add npm run start script --- config/settings.defaults.coffee | 26 ++++++++++++++------------ package.json | 4 ++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 4b391789..fb02c412 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -16,21 +16,10 @@ module.exports = clsiCacheDir: Path.resolve(__dirname + "/../cache") synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) -# clsi: -# commandRunner: "docker-runner-sharelatex" -# docker: -# image: "quay.io/sharelatex/texlive-full:2017.1" -# env: -# HOME: "/tmp" -# socketPath: "/var/run/docker.sock" -# user: "tex" -# expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 -# checkProjectsIntervalMs: 10 * 60 * 1000 - internal: clsi: port: 3013 - host: "localhost" + host: process.env["LISTEN_ADDRESS"] or "localhost" apis: @@ -40,3 +29,16 @@ module.exports = smokeTest: false project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 + +if process.env["COMMAND_RUNNER"] + module.exports.clsi = + commandRunner: process.env["COMMAND_RUNNER"] + docker: + image: process.env["TEXLIVE_IMAGE"] or "quay.io/sharelatex/texlive-full:2017.1" + env: + HOME: "/tmp" + socketPath: "/var/run/docker.sock" + user: "tex" + expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 + checkProjectsIntervalMs: 10 * 60 * 1000 + module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] diff --git a/package.json b/package.json index 71711be1..867bdf23 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" }, + "scripts": { + "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", + "start": "npm run compile:app && node app.js" + }, "author": "James Allen <james@sharelatex.com>", "dependencies": { "async": "0.2.9", From a0d5e6a54bfb380f8acbbe63ad65b28afa5382cb Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Wed, 3 Jan 2018 15:41:31 +0000 Subject: [PATCH 278/709] log an error if core file is found in output --- app/coffee/CompileController.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 99973fdd..1d904056 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -39,6 +39,10 @@ module.exports = CompileController = for file in outputFiles if file.path?.match(/output\.pdf$/) status = "success" + # log an error if any core files are found + for file in outputFiles + if file.path is "core" + logger.error project_id:request.project_id, req:req, outputFiles:outputFiles, "core file found in output" timer.done() res.status(code or 200).send { From 7a6294081d1cb9b03111822f84187610b15863cf Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Tue, 16 Jan 2018 10:46:59 +0000 Subject: [PATCH 279/709] Allow texlive image user to be configured --- config/settings.defaults.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index fb02c412..0e8f6aa3 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -38,7 +38,7 @@ if process.env["COMMAND_RUNNER"] env: HOME: "/tmp" socketPath: "/var/run/docker.sock" - user: "tex" + user: process.env["TEXLIVE_IMAGE_USER"] or "tex" expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 checkProjectsIntervalMs: 10 * 60 * 1000 module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] From b64106b730443c9239b10d006d96b041fc46df8e Mon Sep 17 00:00:00 2001 From: James Allen <james@sharelatex.com> Date: Fri, 29 Dec 2017 08:08:19 +0000 Subject: [PATCH 280/709] Provide hosts and siblings container as environment settings and add npm run start script wip acceptence tests run, but don't all pass wip removed npm-debug from git --- .nvmrc | 2 +- Dockerfile | 19 + Jenkinsfile | 71 +- Makefile | 29 + app.coffee | 5 + bin/acceptance_test | 4 + config/settings.defaults.coffee | 29 +- docker-compose.ci.yml | 33 + docker-compose.yml | 51 + docker-runner | 1 + nodemon.json | 15 + package-lock.json | 3162 ++++++++++++++++++ package.json | 97 +- test/acceptance/coffee/helpers/Client.coffee | 1 + 14 files changed, 3428 insertions(+), 91 deletions(-) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 bin/acceptance_test create mode 100644 docker-compose.ci.yml create mode 100644 docker-compose.yml create mode 160000 docker-runner create mode 100644 nodemon.json create mode 100644 package-lock.json diff --git a/.nvmrc b/.nvmrc index e18a34b9..e1e5d136 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.11.2 +6.9.5 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d98547ca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:6.9.5 + +RUN wget -qO- https://get.docker.com/ | sh + +# ---- Copy Files/Build ---- +WORKDIR /app +COPY ./ /app +# Build react/vue/angular bundle static files +# RUN npm run build +RUN npm install + +RUN npm run compile + +EXPOSE 3013 + +ENV SHARELATEX_CONFIG /app/config/settings.production.coffee +ENV NODE_ENV production + +CMD ["node","/app/app.js"] diff --git a/Jenkinsfile b/Jenkinsfile index 0c289e15..ab90aaae 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,79 +1,72 @@ -pipeline { +String cron_string = BRANCH_NAME == "master" ? "@daily" : "" +pipeline { agent any triggers { pollSCM('* * * * *') - cron('@daily') + cron(cron_string) } stages { - stage('Clean') { - steps { - // This is a terrible hack to set the file ownership to jenkins:jenkins so we can cleanup the directory - sh 'docker run -v $(pwd):/app --rm busybox /bin/chown -R 111:119 /app' - sh 'rm -fr node_modules' - } - } stage('Install') { agent { docker { - image 'node:6.11.2' + image 'node:6.9.5' args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" reuseNode true } } steps { + // we need to disable logallrefupdates, else git clones + // during the npm install will require git to lookup the + // user id which does not exist in the container's + // /etc/passwd file, causing the clone to fail. sh 'git config --global core.logallrefupdates false' - sh 'rm -fr node_modules' - checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: '_docker-runner'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/docker-runner-sharelatex']]]) - sh 'npm install ./_docker-runner' - sh 'rm -fr ./_docker-runner ./_docker-runner@tmp' - sh 'npm install' - sh 'npm rebuild' - sh 'npm install --quiet grunt-cli' + sh 'rm -rf node_modules' + sh 'npm install && npm rebuild' } } - stage('Compile and Test') { + + stage('Compile') { agent { docker { - image 'node:6.11.2' + image 'node:6.9.5' reuseNode true } } steps { - sh 'node_modules/.bin/grunt compile:app' - sh 'node_modules/.bin/grunt compile:acceptance_tests' - sh 'NODE_ENV=development node_modules/.bin/grunt test:unit' + sh 'npm run compile:all' } } - stage('Acceptance Tests') { - environment { - TEXLIVE_IMAGE="quay.io/sharelatex/texlive-full:2017.1" + + stage('Unit Tests') { + steps { + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit' } + } + + stage('Acceptance Tests') { steps { - sh 'mkdir -p compiles cache' - // Not yet running, due to volumes/sibling containers - sh 'docker container prune -f' - sh 'docker pull $TEXLIVE_IMAGE' - sh 'docker pull sharelatex/acceptance-test-runner:clsi-6.11.2' - sh 'docker run --rm -e SIBLING_CONTAINER_USER=root -e SANDBOXED_COMPILES_HOST_DIR=$(pwd)/compiles -e SANDBOXED_COMPILES_SIBLING_CONTAINERS=true -e TEXLIVE_IMAGE=$TEXLIVE_IMAGE -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/app sharelatex/acceptance-test-runner:clsi-6.11.2' - // This is a terrible hack to set the file ownership to jenkins:jenkins so we can cleanup the directory - sh 'docker run -v $(pwd):/app --rm busybox /bin/chown -R 111:119 /app' - sh 'rm -r compiles cache server.log db.sqlite config/settings.defaults.coffee' + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_acceptance' } } - stage('Package') { + + stage('Package and publish build') { steps { sh 'echo ${BUILD_NUMBER} > build_number.txt' sh 'touch build.tar.gz' // Avoid tar warning about files changing during read sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .' + withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { + s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") + } } } - stage('Publish') { + + stage('Publish build number') { steps { + sh 'echo ${BRANCH_NAME}-${BUILD_NUMBER} > build_number.txt' withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { - s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") // The deployment process uses this file to figure out the latest build s3Upload(file:'build_number.txt', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/latest") } @@ -82,6 +75,10 @@ pipeline { } post { + always { + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_clean' + } + failure { mail(from: "${EMAIL_ALERT_FROM}", to: "${EMAIL_ALERT_TO}", diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..6407885d --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/sharelatex/sharelatex-dev-environment +# Version: 1.0.1 + +BUILD_NUMBER ?= local +BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +PROJECT_NAME = clsi +DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml +DOCKER_COMPOSE := docker-compose ${DOCKER_COMPOSE_FLAGS} + +clean: + rm -f app.js + rm -rf app/js + rm -rf test/unit/js + rm -rf test/acceptance/js + +test: test_unit test_acceptance + +test_unit: + @[ -d test/unit ] && $(DOCKER_COMPOSE) run --rm test_unit -- ${MOCHA_ARGS} || echo "clsi has no unit tests" + +test_acceptance: test_clean # clear the database before each acceptance test run + @[ -d test/acceptance ] && $(DOCKER_COMPOSE) run --rm test_acceptance -- ${MOCHA_ARGS} || echo "clsi has no acceptance tests" + +test_clean: + $(DOCKER_COMPOSE) down + +.PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/app.coffee b/app.coffee index 5c79b8ee..ba7d2255 100644 --- a/app.coffee +++ b/app.coffee @@ -132,6 +132,7 @@ resCacher = if Settings.smokeTest do runSmokeTest = -> logger.log("running smoke tests") + console.log(__dirname, __filename) smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher) setTimeout(runSmokeTest, 30 * 1000) @@ -139,6 +140,10 @@ app.get "/health_check", (req, res)-> res.contentType(resCacher?.setContentType) res.status(resCacher?.code).send(resCacher?.body) +app.get "/smoke_test_force", (req, res)-> + smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res) + + profiler = require "v8-profiler" app.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") diff --git a/bin/acceptance_test b/bin/acceptance_test new file mode 100644 index 00000000..fd2e5137 --- /dev/null +++ b/bin/acceptance_test @@ -0,0 +1,4 @@ +#!/bin/bash +set -e; +MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000" +$MOCHA "$@" diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 4b391789..448d13b3 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -16,27 +16,28 @@ module.exports = clsiCacheDir: Path.resolve(__dirname + "/../cache") synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) -# clsi: -# commandRunner: "docker-runner-sharelatex" -# docker: -# image: "quay.io/sharelatex/texlive-full:2017.1" -# env: -# HOME: "/tmp" -# socketPath: "/var/run/docker.sock" -# user: "tex" -# expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 -# checkProjectsIntervalMs: 10 * 60 * 1000 - internal: clsi: port: 3013 - host: "localhost" - + host: process.env["LISTEN_ADDRESS"] or "0.0.0.0" apis: clsi: - url: "http://localhost:3013" + url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" smokeTest: false project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 + +if process.env["COMMAND_RUNNER"] + module.exports.clsi = + commandRunner: process.env["COMMAND_RUNNER"] + docker: + image: process.env["TEXLIVE_IMAGE"] or "quay.io/sharelatex/texlive-full:2017.1" + env: + HOME: "/tmp" + socketPath: "/var/run/docker.sock" + user: process.env["TEXLIVE_IMAGE_USER"] or "tex" + expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 + checkProjectsIntervalMs: 10 * 60 * 1000 + module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 00000000..9f40ba87 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,33 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/sharelatex/sharelatex-dev-environment +# Version: 1.0.1 + +version: "2" + +services: + test_unit: + image: node:6.9.5 + volumes: + - .:/app + working_dir: /app + entrypoint: npm run test:unit:_run + + test_acceptance: + image: node:6.9.5 + volumes: + - .:/app + working_dir: /app + environment: + REDIS_HOST: redis + MONGO_HOST: mongo + depends_on: + - redis + - mongo + entrypoint: npm run test:acceptance:_run + + redis: + image: redis + + mongo: + image: mongo:3.4 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4a25bc38 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/sharelatex/sharelatex-dev-environment +# Version: 1.0.1 + +version: "2" + +services: + test_unit: + image: node:6.9.5 + volumes: + - .:/app + working_dir: /app + entrypoint: npm run test:unit + + test_acceptance: + image: node:6.9.5 + volumes: + - .:/app + environment: + REDIS_HOST: redis + MONGO_HOST: mongo + CLSI_HOST: clsi + depends_on: + - clsi + - redis + - mongo + working_dir: /app + entrypoint: npm run test:acceptance + + redis: + image: redis + + mongo: + image: mongo:3.4 + + clsi: + image: gcr.io/henry-terraform-admin/clsi + build: . + environment: + TEXLIVE_IMAGE: quay.io/sharelatex/texlive-small:latest + TEXLIVE_IMAGE_USER: root # Not ideal, but makes running in dev very simple + COMMAND_RUNNER: docker-runner-sharelatex + SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + COMPILES_HOST_DIR: $PWD/compiles + volumes: + - .:/app:cached + - /var/run/docker.sock:/var/run/docker.sock + - ./docker-runner:/app/node_modules/docker-runner-sharelatex + ports: + - 3013:3013 \ No newline at end of file diff --git a/docker-runner b/docker-runner new file mode 160000 index 00000000..f861a1c8 --- /dev/null +++ b/docker-runner @@ -0,0 +1 @@ +Subproject commit f861a1c810ad844a6e00e82f2ebedac26dcad8b7 diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 00000000..9044f921 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,15 @@ +{ + "ignore": [ + ".git", + "node_modules/" + ], + "verbose": true, + "execMap": { + "js": "npm run start" + }, + "watch": [ + "app/coffee/", + "app.coffee" + ], + "ext": "coffee" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4c110ea5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3162 @@ +{ + "name": "node-clsi", + "version": "0.1.4", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "JSONStream": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", + "integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=", + "requires": { + "jsonparse": "0.0.5", + "through": "2.3.8" + } + }, + "abbrev": { + "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" + }, + "accepts": { + "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "requires": { + "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "negotiator": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" + } + }, + "ajv": { + "version": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "fast-deep-equal": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "fast-json-stable-stringify": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "json-schema-traverse": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz" + } + }, + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", + "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", + "dev": true + }, + "aproba": { + "version": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" + }, + "are-we-there-yet": { + "version": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "requires": { + "delegates": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz" + }, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "requires": { + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" + } + } + } + }, + "argparse": { + "version": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz" + }, + "dependencies": { + "underscore": { + "version": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "array-flatten": { + "version": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "asn1": { + "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", + "dev": true + }, + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", + "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" + }, + "asynckit": { + "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + } + }, + "bignumber.js": { + "version": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", + "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" + }, + "bl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", + "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", + "requires": { + "readable-stream": "2.3.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "1.0.0", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "string_decoder": "1.0.3", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" + } + } + } + }, + "block-stream": { + "version": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + } + }, + "bluebird": { + "version": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz", + "integrity": "sha1-L3tOyAIWMoqf3evfacjUlC/v99g=" + }, + "body-parser": { + "version": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "content-type": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "depd": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "qs": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "raw-body": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz" + } + }, + "boom": { + "version": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz" + } + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + }, + "bunyan": { + "version": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", + "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", + "dev": true, + "requires": { + "dtrace-provider": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", + "mv": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz" + } + }, + "buster-core": { + "version": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", + "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", + "dev": true + }, + "buster-format": { + "version": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", + "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", + "dev": true, + "requires": { + "buster-core": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz" + } + }, + "bytes": { + "version": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "caseless": { + "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", + "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", + "dev": true, + "requires": { + "assertion-error": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "deep-eql": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz" + } + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", + "integrity": "sha1-HJhDdzfxGZ68wdTEj9Qbn5yOjyM=", + "dev": true, + "requires": { + "ansi-styles": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", + "has-color": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz" + } + }, + "co": { + "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "coffee-script": { + "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=", + "dev": true + }, + "colors": { + "version": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "combined-stream": { + "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + } + }, + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "requires": { + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "readable-stream": "2.0.6", + "typedarray": "0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "1.0.0", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + } + } + }, + "console-control-strings": { + "version": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "content-disposition": { + "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" + }, + "cookie": { + "version": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz" + }, + "dependencies": { + "boom": { + "version": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", + "requires": { + "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz" + } + } + } + }, + "dashdash": { + "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + }, + "dateformat": { + "version": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + } + }, + "deep-eql": { + "version": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz" + } + }, + "deep-extend": { + "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, + "delayed-stream": { + "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-libc": { + "version": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "diff": { + "version": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" + }, + "docker-modem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz", + "integrity": "sha512-pkXB9p7KWagegOXm2NsbVDBluQQLCBJzX9uYJzVbL6CHwe4d2sSbcACJ4K8ISX1l1JUUmFSiwNkBKc1uTiU4MA==", + "requires": { + "JSONStream": "0.10.0", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "readable-stream": "1.0.34", + "split-ca": "1.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + } + } + } + }, + "dockerode": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz", + "integrity": "sha512-LQKXR5jyI+G/+5OhZCi40m0ArY4j46g7Tl71Vtn10Ekt5TiyDzZAoqXOCS6edQpEuGbdFgSDJxleFqLxACpKJg==", + "requires": { + "concat-stream": "1.5.2", + "docker-modem": "1.0.4", + "tar-fs": "1.12.0" + } + }, + "dottie": { + "version": "https://registry.npmjs.org/dottie/-/dottie-0.3.1.tgz", + "integrity": "sha1-QYX3d0iZYKwIQTHeP5oUoXm7dJ8=" + }, + "dtrace-provider": { + "version": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", + "integrity": "sha1-4kPxkhmqlfvw2PL/sH9b1k6U/iA=", + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + } + }, + "ee-first": { + "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + } + }, + "escape-html": { + "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "etag": { + "version": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "eventemitter2": { + "version": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "express": { + "version": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", + "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", + "requires": { + "accepts": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "array-flatten": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "body-parser": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "content-disposition": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "content-type": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "cookie": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "cookie-signature": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "depd": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "etag": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "finalhandler": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "merge-descriptors": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "methods": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "proxy-addr": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", + "qs": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "send": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", + "serve-static": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", + "setprototypeof": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "vary": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + }, + "dependencies": { + "setprototypeof": { + "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" + }, + "statuses": { + "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + } + } + }, + "extend": { + "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-json-stable-stringify": { + "version": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "finalhandler": { + "version": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "requires": { + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + }, + "dependencies": { + "statuses": { + "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + } + } + }, + "findup-sync": { + "version": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "requires": { + "glob": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + }, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz" + } + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + } + } + }, + "forever-agent": { + "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "requires": { + "asynckit": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz" + } + }, + "forwarded": { + "version": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-extra": { + "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", + "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "jsonfile": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz" + } + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz" + }, + "dependencies": { + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + } + }, + "fstream-ignore": { + "version": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "requires": { + "fstream": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + } + }, + "gauge": { + "version": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "console-control-strings": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "has-unicode": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "signal-exit": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "wide-align": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz" + } + }, + "generic-pool": { + "version": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.2.0.tgz", + "integrity": "sha1-i0ZcGnWI6p3SuxM72gu2a/74pj4=" + }, + "getobject": { + "version": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "requires": { + "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + } + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "requires": { + "natives": "https://registry.npmjs.org/natives/-/natives-1.1.1.tgz" + } + }, + "growl": { + "version": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" + }, + "grunt": { + "version": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "requires": { + "async": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "dateformat": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "eventemitter2": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "exit": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "findup-sync": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "getobject": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "glob": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "grunt-legacy-log": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "grunt-legacy-util": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "hooker": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "js-yaml": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "nopt": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "which": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" + }, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "coffee-script": { + "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + } + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + }, + "nopt": { + "version": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + } + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "grunt-bunyan": { + "version": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", + "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", + "dev": true, + "requires": { + "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + }, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "grunt-contrib-clean": { + "version": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", + "integrity": "sha1-9T397ghJsce0Dp67umn0jExgecU=", + "dev": true, + "requires": { + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" + }, + "dependencies": { + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "grunt-contrib-coffee": { + "version": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", + "integrity": "sha1-ixIme3TnM4sfKcW4txj7n4mYLxM=", + "dev": true, + "requires": { + "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz" + }, + "dependencies": { + "coffee-script": { + "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", + "integrity": "sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4=", + "dev": true + } + } + }, + "grunt-execute": { + "version": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", + "integrity": "sha1-yX64lDYS/vu3L749Mu+VIzxfouk=", + "dev": true + }, + "grunt-legacy-log": { + "version": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "grunt-legacy-log-utils": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "hooker": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" + }, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" + }, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "exit": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "getobject": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "hooker": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "which": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" + }, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + } + } + }, + "grunt-mkdir": { + "version": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz", + "integrity": "sha1-c+GiasJKCFljY/TdlUsNMkheWOk=" + }, + "grunt-mocha-test": { + "version": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", + "integrity": "sha1-emGEuYhg0Phb3qrWvqob199bvus=", + "dev": true, + "requires": { + "mocha": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz" + }, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "dev": true, + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + } + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + }, + "mocha": { + "version": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", + "integrity": "sha1-cT223FAAGRqdA1gZXQkIeQ7LYVc=", + "dev": true, + "requires": { + "commander": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "diff": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "glob": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "growl": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "jade": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + } + } + } + }, + "grunt-shell": { + "version": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", + "integrity": "sha1-5KbRuSkSd2/ZOimcX2zGTpUlNlw=", + "dev": true, + "requires": { + "chalk": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz" + } + }, + "har-schema": { + "version": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "har-schema": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + } + }, + "has-color": { + "version": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "has-unicode": { + "version": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hawk": { + "version": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", + "requires": { + "boom": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "cryptiles": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "sntp": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz" + } + }, + "heapdump": { + "version": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.9.tgz", + "integrity": "sha1-A8dOsN9dZ74Jgug0KbqcnSs7f3g=" + }, + "hoek": { + "version": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=" + }, + "hooker": { + "version": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "http-errors": { + "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "setprototypeof": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + }, + "dependencies": { + "depd": { + "version": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + } + } + }, + "http-signature": { + "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "jsprim": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "sshpk": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz" + } + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=" + }, + "inflection": { + "version": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" + }, + "ipaddr.js": { + "version": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", + "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" + }, + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + } + }, + "is-typedarray": { + "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isstream": { + "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jade": { + "version": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "requires": { + "commander": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + }, + "dependencies": { + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" + } + } + }, + "js-yaml": { + "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "esprima": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" + } + }, + "jsbn": { + "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" + } + }, + "json-stringify-safe": { + "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" + }, + "dependencies": { + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "optional": true + } + } + }, + "jsonify": { + "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonparse": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", + "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=" + }, + "jsprim": { + "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "extsprintf": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "json-schema": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "verror": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + } + }, + "lockfile": { + "version": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.3.tgz", + "integrity": "sha1-Jjj8OaAzHpysGgS3F5mTHJxQ33k=" + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "logger-sharelatex": { + "version": "git+https://github.com/sharelatex/logger-sharelatex.git#9ee7b52eb2bbd8fcbb1e2c708587c1e93fd4c733", + "integrity": "sha1-aRMA+7GVHSmsRMbyj5cLhnueM9Q=", + "requires": { + "bunyan": "1.5.1", + "coffee-script": "1.4.0", + "raven": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz" + }, + "dependencies": { + "bunyan": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", + "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", + "requires": { + "dtrace-provider": "0.6.0", + "mv": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "safe-json-stringify": "1.0.4" + } + }, + "coffee-script": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", + "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" + }, + "dtrace-provider": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", + "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", + "optional": true, + "requires": { + "nan": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz" + } + } + } + }, + "lru-cache": { + "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "lsmod": { + "version": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", + "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" + }, + "lynx": { + "version": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", + "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", + "requires": { + "mersenne": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "statsd-parser": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz" + } + }, + "media-typer": { + "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "mersenne": { + "version": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" + }, + "methods": { + "version": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "metrics-sharelatex": { + "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#05bd57604115ae5efdca2a419fbef4f25e9a780f", + "integrity": "sha1-ruLAc3Tl1GZrAQjP/K4NeRajdW4=", + "requires": { + "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "lynx": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", + "underscore": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" + }, + "dependencies": { + "coffee-script": { + "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + }, + "lynx": { + "version": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", + "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", + "requires": { + "mersenne": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "statsd-parser": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz" + } + }, + "underscore": { + "version": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" + } + } + }, + "mime": { + "version": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=" + }, + "mime-db": { + "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz" + } + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz" + } + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" + }, + "mocha": { + "version": "https://registry.npmjs.org/mocha/-/mocha-1.10.0.tgz", + "integrity": "sha1-8WrMlQ75Vm+/kIvPttXQQWw50No=", + "dev": true, + "requires": { + "commander": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "diff": "https://registry.npmjs.org/diff/-/diff-1.0.2.tgz", + "glob": "https://registry.npmjs.org/glob/-/glob-3.2.1.tgz", + "growl": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "jade": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.3.tgz", + "ms": "https://registry.npmjs.org/ms/-/ms-0.3.0.tgz" + }, + "dependencies": { + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "diff": { + "version": "https://registry.npmjs.org/diff/-/diff-1.0.2.tgz", + "integrity": "sha1-Suc/Gu6Nb89ITxoc53zmUdm38Mk=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.2.1.tgz", + "integrity": "sha1-V69w7HO6IyO/4/KaBndl22TF11g=", + "dev": true, + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + } + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.3.tgz", + "integrity": "sha1-WV4lHBNww6aLqyE20ONIuBBa3xM=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.3.0.tgz", + "integrity": "sha1-A+3DSNYT5mpWSGz9rFO8vomcvWE=", + "dev": true + } + } + }, + "moment": { + "version": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha1-1usaRsvMFKKy+UNBEsH/iQfzE/0=" + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mv": { + "version": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "ncp": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz" + }, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + } + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz" + } + } + } + }, + "mysql": { + "version": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", + "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", + "requires": { + "bignumber.js": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", + "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "require-all": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz" + } + }, + "nan": { + "version": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" + }, + "natives": { + "version": "https://registry.npmjs.org/natives/-/natives-1.1.1.tgz", + "integrity": "sha1-ARrM4ffL2H97prMJPWzZOSvhxXQ=" + }, + "ncp": { + "version": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "negotiator": { + "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "node-pre-gyp": { + "version": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", + "integrity": "sha1-wA6WhgsjwOFCCse+/FBE4deNhkk=", + "requires": { + "detect-libc": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "hawk": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "nopt": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "npmlog": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "rc": "https://registry.npmjs.org/rc/-/rc-1.2.3.tgz", + "request": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "semver": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "tar": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "tar-pack": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz" + }, + "dependencies": { + "ajv": { + "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "json-stable-stringify": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" + } + }, + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "aws-sign2": { + "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "boom": { + "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + } + }, + "cryptiles": { + "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + } + }, + "form-data": { + "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz" + } + }, + "har-schema": { + "version": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "har-schema": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz" + } + }, + "hawk": { + "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "cryptiles": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "hoek": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "sntp": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + } + }, + "hoek": { + "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "http-signature": { + "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "jsprim": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "sshpk": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz" + } + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + }, + "performance-now": { + "version": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "request": { + "version": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "caseless": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "form-data": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "har-validator": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "hawk": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "http-signature": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "performance-now": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "tunnel-agent": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "uuid": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz" + } + }, + "sntp": { + "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + } + } + } + }, + "nopt": { + "version": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "osenv": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz" + } + }, + "npmlog": { + "version": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "requires": { + "are-we-there-yet": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "console-control-strings": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "gauge": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "set-blocking": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + } + }, + "number-is-nan": { + "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + } + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + }, + "os-homedir": { + "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "requires": { + "os-homedir": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + } + }, + "parseurl": { + "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-to-regexp": { + "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "performance-now": { + "version": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "proxy-addr": { + "version": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", + "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "requires": { + "forwarded": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "ipaddr.js": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz" + } + }, + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "1.4.1", + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + } + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" + }, + "range-parser": { + "version": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raven": { + "version": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", + "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", + "requires": { + "cookie": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "lsmod": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", + "stack-trace": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "uuid": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz" + } + }, + "raw-body": { + "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + } + }, + "rc": { + "version": "https://registry.npmjs.org/rc/-/rc-1.2.3.tgz", + "integrity": "sha1-UVdakA+N1oOBxxC0cSwhVMPiA1s=", + "requires": { + "deep-extend": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "ini": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "minimist": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "strip-json-comments": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + }, + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + } + }, + "request": { + "version": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", + "requires": { + "aws-sign2": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "caseless": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "form-data": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "har-validator": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "hawk": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "http-signature": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "performance-now": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "qs": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "tunnel-agent": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "uuid": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz" + }, + "dependencies": { + "uuid": { + "version": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" + } + } + }, + "require-all": { + "version": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", + "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" + }, + "require-like": { + "version": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", + "dev": true + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "requires": { + "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" + } + }, + "safe-buffer": { + "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" + }, + "safe-json-stringify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", + "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", + "optional": true + }, + "sandboxed-module": { + "version": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", + "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", + "dev": true, + "requires": { + "require-like": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "stack-trace": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz" + }, + "dependencies": { + "stack-trace": { + "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", + "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", + "dev": true + } + } + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" + }, + "send": { + "version": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", + "integrity": "sha1-pw4coh0TgsEdDZ9iMd6ygQgNerM=", + "requires": { + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "depd": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "destroy": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "etag": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "mime": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" + }, + "dependencies": { + "statuses": { + "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + } + } + }, + "sequelize": { + "version": "https://registry.npmjs.org/sequelize/-/sequelize-2.1.3.tgz", + "integrity": "sha1-FrpROxNnlY/oIzhYEelRFFHt6tE=", + "requires": { + "bluebird": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz", + "dottie": "https://registry.npmjs.org/dottie/-/dottie-0.3.1.tgz", + "generic-pool": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.2.0.tgz", + "inflection": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "moment": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "node-uuid": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "toposort-class": "https://registry.npmjs.org/toposort-class/-/toposort-class-0.3.1.tgz", + "validator": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz" + }, + "dependencies": { + "node-uuid": { + "version": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + } + } + }, + "serve-static": { + "version": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha1-TFfVNASnYdjy58HooYpH2/J4pxk=", + "requires": { + "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "send": "https://registry.npmjs.org/send/-/send-0.16.1.tgz" + } + }, + "set-blocking": { + "version": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "settings-sharelatex": { + "version": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", + "integrity": "sha1-RatFGqtZ7wuhO78vGSB60S3H9Rc=", + "requires": { + "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" + }, + "dependencies": { + "coffee-script": { + "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + } + } + }, + "sigmund": { + "version": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "signal-exit": { + "version": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sinon": { + "version": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", + "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", + "dev": true, + "requires": { + "buster-format": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz" + } + }, + "smoke-test-sharelatex": { + "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", + "integrity": "sha1-s6idlk0vuV2Kz+u6rn3ITbaMEQ4=", + "requires": { + "mocha": "1.17.1" + }, + "dependencies": { + "glob": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "requires": { + "graceful-fs": "2.0.3", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "requires": { + "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + }, + "mocha": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", + "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", + "requires": { + "commander": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "diff": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "glob": "3.2.3", + "growl": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "jade": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + } + } + } + }, + "sntp": { + "version": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", + "requires": { + "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz" + } + }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, + "sqlite3": { + "version": "https://registry.npmjs.org/sqlite3/-/sqlite3-3.1.13.tgz", + "integrity": "sha1-2ZCgVic5J2jeYni6/Rox/f6Qfdk=", + "requires": { + "nan": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "node-pre-gyp": "0.6.38" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, + "node-pre-gyp": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz", + "integrity": "sha1-6Sog+DQWQVu0CG9tH7eLPac9ET0=", + "requires": { + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", + "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, + "sshpk": { + "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "bcrypt-pbkdf": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "dashdash": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "ecc-jsbn": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "getpass": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "jsbn": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "tweetnacl": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + } + }, + "stack-trace": { + "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" + }, + "statsd-parser": { + "version": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", + "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" + }, + "statuses": { + "version": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + } + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringstream": { + "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + } + }, + "strip-json-comments": { + "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar": { + "version": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "fstream": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + } + }, + "tar-fs": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", + "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", + "requires": { + "mkdirp": "0.5.1", + "pump": "1.0.3", + "tar-stream": "1.5.5" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + } + }, + "tar-pack": { + "version": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "integrity": "sha1-4dvAOpudO6B+iWrQJzF+tnmhCh8=", + "requires": { + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "fstream": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "fstream-ignore": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "tar": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "uid-number": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" + }, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "requires": { + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" + } + } + } + }, + "tar-stream": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", + "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", + "requires": { + "bl": "1.2.1", + "end-of-stream": "1.4.1", + "readable-stream": "2.3.3", + "xtend": "4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "1.0.0", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "string_decoder": "1.0.3", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" + } + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "timekeeper": { + "version": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", + "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", + "dev": true + }, + "toposort-class": { + "version": "https://registry.npmjs.org/toposort-class/-/toposort-class-0.3.1.tgz", + "integrity": "sha1-0TM67XNPhgjO0ShiGL0sRfYO/0g=" + }, + "tough-cookie": { + "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + } + }, + "tunnel-agent": { + "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" + } + }, + "tweetnacl": { + "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + }, + "type-is": { + "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uid-number": { + "version": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + }, + "underscore": { + "version": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "unpipe": { + "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", + "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + }, + "v8-profiler": { + "version": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", + "integrity": "sha1-6DgcvrtbX9DKjSsJ9qAYGhWNs00=", + "requires": { + "nan": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "node-pre-gyp": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz" + } + }, + "validator": { + "version": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz", + "integrity": "sha1-lkZLmS1BloM9l6GUv0Cxn/VLrgU=" + }, + "vary": { + "version": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "extsprintf": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + } + }, + "which": { + "version": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "wide-align": { + "version": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", + "requires": { + "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" + } + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "wrench": { + "version": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", + "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/package.json b/package.json index 71711be1..efc7aa0d 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,67 @@ { - "name": "node-clsi", - "description": "A Node.js implementation of the CLSI LaTeX web-API", - "version": "0.1.4", + "name": "node-clsi", + "description": "A Node.js implementation of the CLSI LaTeX web-API", + "version": "0.1.4", "repository": { - "type": "git", + "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" - }, - "author": "James Allen <james@sharelatex.com>", + }, + "scripts": { + "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3013/status) do sleep 1; done", + "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000", + "test:acceptance:dir": "npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", + "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", + "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", + "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", + "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", + "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", + "start": "npm run compile:app && node app.js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 $@ test/acceptance/js", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", + "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", + "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", + "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && compile:test:smoke", + "nodemon": "nodemon --config nodemon.json" + }, + "author": "James Allen <james@sharelatex.com>", "dependencies": { - "async": "0.2.9", - "body-parser": "^1.2.0", - "express": "^4.2.0", - "fs-extra": "^0.16.3", - "grunt-mkdir": "^1.0.0", - "heapdump": "^0.3.5", - "lockfile": "^1.0.3", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", - "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", - "mkdirp": "0.3.5", - "mysql": "2.6.2", - "request": "^2.21.0", - "sequelize": "^2.1.3", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~3.1.8", - "underscore": "^1.8.2", - "v8-profiler": "^5.2.4", + "async": "0.2.9", + "body-parser": "^1.2.0", + "dockerode": "^2.5.3", + "express": "^4.2.0", + "fs-extra": "^0.16.3", + "grunt-mkdir": "^1.0.0", + "heapdump": "^0.3.5", + "lockfile": "^1.0.3", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", + "lynx": "0.0.11", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", + "mkdirp": "0.3.5", + "mysql": "2.6.2", + "request": "^2.21.0", + "sequelize": "^2.1.3", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", + "sqlite3": "~3.1.8", + "underscore": "^1.8.2", + "v8-profiler": "^5.2.4", "wrench": "~1.5.4" - }, + }, "devDependencies": { - "mocha": "1.10.0", - "coffee-script": "1.6.0", - "chai": "~1.8.1", - "sinon": "~1.7.3", - "grunt": "~0.4.2", - "grunt-contrib-coffee": "~0.7.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-shell": "~0.6.1", - "grunt-mocha-test": "~0.8.1", - "sandboxed-module": "~0.3.0", - "timekeeper": "0.0.4", - "grunt-execute": "^0.1.5", - "bunyan": "^0.22.1", + "mocha": "1.10.0", + "coffee-script": "1.6.0", + "chai": "~1.8.1", + "sinon": "~1.7.3", + "grunt": "~0.4.2", + "grunt-contrib-coffee": "~0.7.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-shell": "~0.6.1", + "grunt-mocha-test": "~0.8.1", + "sandboxed-module": "~0.3.0", + "timekeeper": "0.0.4", + "grunt-execute": "^0.1.5", + "bunyan": "^0.22.1", "grunt-bunyan": "^0.5.0" } } diff --git a/test/acceptance/coffee/helpers/Client.coffee b/test/acceptance/coffee/helpers/Client.coffee index c67b4251..76634966 100644 --- a/test/acceptance/coffee/helpers/Client.coffee +++ b/test/acceptance/coffee/helpers/Client.coffee @@ -11,6 +11,7 @@ module.exports = Client = Math.random().toString(16).slice(2) compile: (project_id, data, callback = (error, res, body) ->) -> + console.log("#{@host}/project/#{project_id}/compile") request.post { url: "#{@host}/project/#{project_id}/compile" json: From 017ba3a4ece03788dd9950daa2f613bf73a0a623 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 13 Feb 2018 12:05:10 +0000 Subject: [PATCH 281/709] mvp needs hacked pacth in docker runner wip most tests pass --- .gitignore | 1 + Dockerfile | 16 +- Jenkinsfile | 36 +- Makefile | 5 + app/coffee/CommandRunner.coffee | 2 + app/coffee/CompileController.coffee | 3 + app/coffee/CompileManager.coffee | 12 +- app/coffee/LatexRunner.coffee | 1 + app/coffee/OutputFileFinder.coffee | 2 - app/coffee/ResourceWriter.coffee | 1 + bin/synctex | Bin 0 -> 90472 bytes config/settings.defaults.coffee | 3 + docker-compose.ci.yml | 10 +- docker-compose.yml | 8 +- docker-runner | 2 +- package-lock.json | 5264 ++++++++++------- package.json | 112 +- .../coffee/ExampleDocumentTests.coffee | 3 +- test/acceptance/coffee/helpers/Client.coffee | 1 + 19 files changed, 3289 insertions(+), 2193 deletions(-) create mode 100755 bin/synctex diff --git a/.gitignore b/.gitignore index 99e97608..476f2d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ cache db.sqlite config/* bin/synctex +npm-debug.log diff --git a/Dockerfile b/Dockerfile index d98547ca..83e452d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,17 +2,21 @@ FROM node:6.9.5 RUN wget -qO- https://get.docker.com/ | sh -# ---- Copy Files/Build ---- -WORKDIR /app +run apt-get install poppler-utils vim ghostscript --yes + +# run git build-essential --yes +# RUN git clone https://github.com/netblue30/firejail.git +# RUN cd firejail && ./configure && make && make install-strip +# run mkdir /data + COPY ./ /app -# Build react/vue/angular bundle static files -# RUN npm run build + +WORKDIR /app + RUN npm install RUN npm run compile -EXPOSE 3013 - ENV SHARELATEX_CONFIG /app/config/settings.production.coffee ENV NODE_ENV production diff --git a/Jenkinsfile b/Jenkinsfile index ab90aaae..bc9ba014 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,34 +9,9 @@ pipeline { } stages { - stage('Install') { - agent { - docker { - image 'node:6.9.5' - args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" - reuseNode true - } - } - steps { - // we need to disable logallrefupdates, else git clones - // during the npm install will require git to lookup the - // user id which does not exist in the container's - // /etc/passwd file, causing the clone to fail. - sh 'git config --global core.logallrefupdates false' - sh 'rm -rf node_modules' - sh 'npm install && npm rebuild' - } - } - - stage('Compile') { - agent { - docker { - image 'node:6.9.5' - reuseNode true - } - } + stage('Build') { steps { - sh 'npm run compile:all' + sh 'make build' } } @@ -54,12 +29,7 @@ pipeline { stage('Package and publish build') { steps { - sh 'echo ${BUILD_NUMBER} > build_number.txt' - sh 'touch build.tar.gz' // Avoid tar warning about files changing during read - sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .' - withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { - s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") - } + sh 'make publish' } } diff --git a/Makefile b/Makefile index 6407885d..6e7fca5f 100644 --- a/Makefile +++ b/Makefile @@ -25,5 +25,10 @@ test_acceptance: test_clean # clear the database before each acceptance test run test_clean: $(DOCKER_COMPOSE) down +build: + docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . + +publish: + docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index f47af001..969b55f3 100644 --- a/app/coffee/CommandRunner.coffee +++ b/app/coffee/CommandRunner.coffee @@ -5,7 +5,9 @@ logger.info "using standard command runner" module.exports = CommandRunner = run: (project_id, command, directory, image, timeout, environment, callback = (error) ->) -> + console.log("Command runner", directory) command = (arg.replace('$COMPILE_DIR', directory) for arg in command) + console.log("Command runner 2", command) logger.log project_id: project_id, command: command, directory: directory, "running command" logger.warn "timeouts and sandboxing are not enabled with CommandRunner" diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 99973fdd..52c0e147 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -34,11 +34,14 @@ module.exports = CompileController = status = "error" code = 500 logger.error err: error, project_id: request.project_id, "error running compile" + else status = "failure" for file in outputFiles if file.path?.match(/output\.pdf$/) status = "success" + if status == "failure" + logger.err project_id: request.project_id, outputFiles:outputFiles, "project failed to compile successfully, no output.pdf generated" timer.done() res.status(code or 200).send { diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 167a80e1..d3d319af 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -41,6 +41,7 @@ module.exports = CompileManager = doCompile: (request, callback = (error, outputFiles) ->) -> compileDir = getCompileDir(request.project_id, request.user_id) + console.log("doCompile",compileDir ) timer = new Metrics.Timer("write-to-disk") logger.log project_id: request.project_id, user_id: request.user_id, "syncing resources to disk" @@ -206,9 +207,14 @@ module.exports = CompileManager = file_path = base_dir + "/" + file_name compileDir = getCompileDir(project_id, user_id) synctex_path = Path.join(compileDir, "output.pdf") - CompileManager._runSynctex ["code", synctex_path, file_path, line, column], (error, stdout) -> + command = ["code", synctex_path, file_path, line, column] + CompileManager._runSynctex command, (error, stdout) -> return callback(error) if error? - logger.log project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, stdout: stdout, "synctex code output" + if stdout.toLowerCase().indexOf("warning") == -1 + logType = "log" + else + logType = "err" + logger[logType] project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, command:command, stdout: stdout, "synctex code output" callback null, CompileManager._parseSynctexFromCodeOutput(stdout) syncFromPdf: (project_id, user_id, page, h, v, callback = (error, filePositions) ->) -> @@ -216,6 +222,7 @@ module.exports = CompileManager = base_dir = Settings.path.synctexBaseDir(compileName) compileDir = getCompileDir(project_id, user_id) synctex_path = Path.join(compileDir, "output.pdf") + logger.log({base_dir, project_id, synctex_path}, "base diiir") CompileManager._runSynctex ["pdf", synctex_path, page, h, v], (error, stdout) -> return callback(error) if error? logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" @@ -243,6 +250,7 @@ module.exports = CompileManager = return callback(error) if error? if Settings.clsi?.synctexCommandWrapper? [bin_path, args] = Settings.clsi?.synctexCommandWrapper bin_path, args + logger.log({bin_path, args}, "synctex being run") child_process.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) -> if error? logger.err err:error, args:args, "error running synctex" diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 6a5a4f6a..11e71e53 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -8,6 +8,7 @@ ProcessTable = {} # table of currently running jobs (pids or docker container n module.exports = LatexRunner = runLatex: (project_id, options, callback = (error) ->) -> + console.log("LatexRunner", options.directory) {directory, mainFile, compiler, timeout, image, environment} = options compiler ||= "pdflatex" timeout ||= 60000 # milliseconds diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index 4b07f6e1..662440b5 100644 --- a/app/coffee/OutputFileFinder.coffee +++ b/app/coffee/OutputFileFinder.coffee @@ -10,8 +10,6 @@ module.exports = OutputFileFinder = for resource in resources incomingResources[resource.path] = true - logger.log directory: directory, "getting output files" - OutputFileFinder._getAllFiles directory, (error, allFiles = []) -> if error? logger.err err:error, "error finding all output files" diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 0b6aef5b..66cfbfa0 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -109,6 +109,7 @@ module.exports = ResourceWriter = callback() _writeResourceToDisk: (project_id, resource, basePath, callback = (error) ->) -> + console.log("_writeResourceToDisk", basePath, resource.path) ResourceWriter.checkPath basePath, resource.path, (error, path) -> return callback(error) if error? mkdirp Path.dirname(path), (error) -> diff --git a/bin/synctex b/bin/synctex new file mode 100755 index 0000000000000000000000000000000000000000..99044b97a0259b30afbf0bfb34365e4f1270479e GIT binary patch literal 90472 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:nwe6m+2D>* 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 Ww?EQ8J?~lm(LUq<N}>K2<Nq%(A5^6P literal 0 HcmV?d00001 diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 448d13b3..4835b4fe 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -40,4 +40,7 @@ if process.env["COMMAND_RUNNER"] user: process.env["TEXLIVE_IMAGE_USER"] or "tex" expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 checkProjectsIntervalMs: 10 * 60 * 1000 + module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] + +console.log module.exports \ No newline at end of file diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 9f40ba87..f34280d8 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -7,17 +7,11 @@ version: "2" services: test_unit: - image: node:6.9.5 - volumes: - - .:/app - working_dir: /app + image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER entrypoint: npm run test:unit:_run test_acceptance: - image: node:6.9.5 - volumes: - - .:/app - working_dir: /app + image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER environment: REDIS_HOST: redis MONGO_HOST: mongo diff --git a/docker-compose.yml b/docker-compose.yml index 4a25bc38..d09ea6e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,9 +20,7 @@ services: environment: REDIS_HOST: redis MONGO_HOST: mongo - CLSI_HOST: clsi depends_on: - - clsi - redis - mongo working_dir: /app @@ -34,18 +32,18 @@ services: mongo: image: mongo:3.4 - clsi: + app: image: gcr.io/henry-terraform-admin/clsi build: . environment: - TEXLIVE_IMAGE: quay.io/sharelatex/texlive-small:latest + TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: root # Not ideal, but makes running in dev very simple COMMAND_RUNNER: docker-runner-sharelatex SHARELATEX_CONFIG: /app/config/settings.defaults.coffee COMPILES_HOST_DIR: $PWD/compiles volumes: - - .:/app:cached - /var/run/docker.sock:/var/run/docker.sock - ./docker-runner:/app/node_modules/docker-runner-sharelatex + - /Users/henry/Projects/sharelatex-dev-environment/clsi/compiles:/app/compiles ports: - 3013:3013 \ No newline at end of file diff --git a/docker-runner b/docker-runner index f861a1c8..9a732b25 160000 --- a/docker-runner +++ b/docker-runner @@ -1 +1 @@ -Subproject commit f861a1c810ad844a6e00e82f2ebedac26dcad8b7 +Subproject commit 9a732b2594f014a722f0c16150cf848b9512430f diff --git a/package-lock.json b/package-lock.json index 4c110ea5..5f30ca54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,145 +13,11 @@ "through": "2.3.8" } }, - "abbrev": { - "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" - }, - "accepts": { - "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", - "requires": { - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "negotiator": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" - } - }, - "ajv": { - "version": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "fast-deep-equal": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "fast-json-stable-stringify": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "json-schema-traverse": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz" - } - }, - "ansi-regex": { - "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", - "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", - "dev": true - }, - "aproba": { - "version": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" - }, - "are-we-there-yet": { - "version": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "requires": { - "delegates": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz" - }, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", - "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - } - } - } - }, - "argparse": { - "version": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", - "dev": true, - "requires": { - "underscore": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz" - }, - "dependencies": { - "underscore": { - "version": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", - "dev": true - }, - "underscore.string": { - "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", - "dev": true - } - } - }, - "array-flatten": { - "version": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asn1": { - "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", - "dev": true - }, "async": { - "version": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" }, - "asynckit": { - "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "balanced-match": { - "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt-pbkdf": { - "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - } - }, - "bignumber.js": { - "version": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", - "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" - }, "bl": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", @@ -196,1854 +62,3374 @@ "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" } }, - "bluebird": { - "version": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz", - "integrity": "sha1-L3tOyAIWMoqf3evfacjUlC/v99g=" - }, "body-parser": { - "version": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", "requires": { - "bytes": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "content-type": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "depd": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "qs": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "raw-body": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz" - } - }, - "boom": { - "version": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz" - } - }, - "brace-expansion": { - "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - } - }, - "bunyan": { - "version": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", - "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", - "dev": true, - "requires": { - "dtrace-provider": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", - "mv": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz" - } - }, - "buster-core": { - "version": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", - "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", - "dev": true - }, - "buster-format": { - "version": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", - "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", - "dev": true, - "requires": { - "buster-core": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz" - } - }, - "bytes": { - "version": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "caseless": { - "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", - "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", - "dev": true, - "requires": { - "assertion-error": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "deep-eql": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz" - } - }, - "chalk": { - "version": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", - "integrity": "sha1-HJhDdzfxGZ68wdTEj9Qbn5yOjyM=", - "dev": true, - "requires": { - "ansi-styles": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", - "has-color": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz" - } - }, - "co": { - "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "coffee-script": { - "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=", - "dev": true - }, - "colors": { - "version": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "dev": true - }, - "combined-stream": { - "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - } - }, - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" - }, - "concat-map": { - "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "requires": { - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "readable-stream": "2.0.6", - "typedarray": "0.0.6" + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.15" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "1.0.0", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + "ms": "2.0.0" } - } - } - }, - "console-control-strings": { - "version": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "content-disposition": { - "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" - }, - "cookie": { - "version": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "core-util-is": { - "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cryptiles": { - "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz" - }, - "dependencies": { - "boom": { - "version": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.4.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + } + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", "requires": { - "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz" + "media-typer": "0.3.0", + "mime-types": "2.1.17" } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" } } }, - "dashdash": { - "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - }, - "dateformat": { - "version": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "requires": { - "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - } - }, - "deep-eql": { - "version": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "bunyan": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", + "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", "dev": true, "requires": { - "type-detect": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz" - } - }, - "deep-extend": { - "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, - "delayed-stream": { - "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-libc": { - "version": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "diff": { - "version": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" - }, - "docker-modem": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz", - "integrity": "sha512-pkXB9p7KWagegOXm2NsbVDBluQQLCBJzX9uYJzVbL6CHwe4d2sSbcACJ4K8ISX1l1JUUmFSiwNkBKc1uTiU4MA==", - "requires": { - "JSONStream": "0.10.0", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "readable-stream": "1.0.34", - "split-ca": "1.0.1" + "mv": "2.1.1" }, "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + "balanced-match": "1.0.0", + "concat-map": "0.0.1" } - } - } - }, - "dockerode": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz", - "integrity": "sha512-LQKXR5jyI+G/+5OhZCi40m0ArY4j46g7Tl71Vtn10Ekt5TiyDzZAoqXOCS6edQpEuGbdFgSDJxleFqLxACpKJg==", - "requires": { - "concat-stream": "1.5.2", - "docker-modem": "1.0.4", - "tar-fs": "1.12.0" - } - }, - "dottie": { - "version": "https://registry.npmjs.org/dottie/-/dottie-0.3.1.tgz", - "integrity": "sha1-QYX3d0iZYKwIQTHeP5oUoXm7dJ8=" - }, - "dtrace-provider": { - "version": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", - "integrity": "sha1-4kPxkhmqlfvw2PL/sH9b1k6U/iA=", - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" - } - }, - "ee-first": { - "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - } - }, - "escape-html": { - "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", - "dev": true - }, - "etag": { - "version": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "eventemitter2": { - "version": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true - }, - "exit": { - "version": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "express": { - "version": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", - "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", - "requires": { - "accepts": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "array-flatten": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "body-parser": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "content-disposition": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "content-type": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "cookie": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "cookie-signature": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "depd": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "etag": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "finalhandler": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "merge-descriptors": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "methods": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "proxy-addr": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "qs": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "send": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "serve-static": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "setprototypeof": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "vary": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - }, - "dependencies": { - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" }, - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - } - } - }, - "extend": { - "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extsprintf": { - "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" - }, - "fast-json-stable-stringify": { - "version": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "finalhandler": { - "version": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "requires": { - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - }, - "dependencies": { - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - } - } - }, - "findup-sync": { - "version": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", - "dev": true, - "requires": { - "glob": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" - }, - "dependencies": { + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true, + "optional": true + }, "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", "dev": true, + "optional": true, "requires": { - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz" + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" } }, - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true, + "optional": true }, "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { - "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + "brace-expansion": "1.1.11" } - } - } - }, - "forever-agent": { - "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz" - } - }, - "forwarded": { - "version": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-extra": { - "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", - "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", - "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "jsonfile": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz" - } - }, - "fs.realpath": { - "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz" - }, - "dependencies": { - "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true, + "optional": true }, "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "optional": true, "requires": { - "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "minimist": "0.0.8" } - } - } - }, - "fstream-ignore": { - "version": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "requires": { - "fstream": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - } - }, - "gauge": { - "version": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "console-control-strings": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "has-unicode": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "signal-exit": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "wide-align": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz" - } - }, - "generic-pool": { - "version": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.2.0.tgz", - "integrity": "sha1-i0ZcGnWI6p3SuxM72gu2a/74pj4=" - }, - "getobject": { - "version": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", - "dev": true - }, - "getpass": { - "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "requires": { - "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - } - }, - "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "requires": { - "natives": "https://registry.npmjs.org/natives/-/natives-1.1.1.tgz" - } - }, - "growl": { - "version": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" - }, - "grunt": { - "version": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", - "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", - "dev": true, - "requires": { - "async": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", - "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "dateformat": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "eventemitter2": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "exit": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "findup-sync": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "getobject": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "glob": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "grunt-legacy-log": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "grunt-legacy-util": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "hooker": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "js-yaml": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "nopt": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "which": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" - }, - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", - "dev": true }, - "coffee-script": { - "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", - "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", - "dev": true - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", "dev": true, + "optional": true, "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" } }, - "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", - "dev": true - }, - "iconv-lite": { - "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", - "dev": true - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", - "dev": true - }, - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", - "dev": true + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true, + "optional": true }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + "wrappy": "1.0.2" } }, - "nopt": { - "version": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "optional": true + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", "dev": true, + "optional": true, "requires": { - "abbrev": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + "glob": "6.0.4" } }, - "rimraf": { - "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true } } }, - "grunt-bunyan": { - "version": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", - "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", + "chai": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", + "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", "dev": true, "requires": { - "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + "assertion-error": "1.0.0", + "deep-eql": "0.1.3" }, "dependencies": { - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "assertion-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", "dev": true - } - } - }, - "grunt-contrib-clean": { - "version": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", - "integrity": "sha1-9T397ghJsce0Dp67umn0jExgecU=", - "dev": true, - "requires": { - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" - }, - "dependencies": { - "rimraf": { - "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + } + }, + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", "dev": true } } }, - "grunt-contrib-coffee": { - "version": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", - "integrity": "sha1-ixIme3TnM4sfKcW4txj7n4mYLxM=", - "dev": true, + "coffee-script": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=", + "dev": true + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", "requires": { - "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz" + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "readable-stream": "2.0.6", + "typedarray": "0.0.6" }, "dependencies": { - "coffee-script": { - "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", - "integrity": "sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4=", - "dev": true + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "1.0.0", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } } } }, - "grunt-execute": { - "version": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", - "integrity": "sha1-yX64lDYS/vu3L749Mu+VIzxfouk=", - "dev": true + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "grunt-legacy-log": { - "version": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", - "dev": true, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", "requires": { - "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "grunt-legacy-log-utils": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "hooker": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" - }, - "dependencies": { - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "underscore.string": { - "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", - "dev": true - } + "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" } }, - "grunt-legacy-log-utils": { - "version": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", - "dev": true, + "docker-modem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz", + "integrity": "sha512-pkXB9p7KWagegOXm2NsbVDBluQQLCBJzX9uYJzVbL6CHwe4d2sSbcACJ4K8ISX1l1JUUmFSiwNkBKc1uTiU4MA==", "requires": { - "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" + "JSONStream": "0.10.0", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "readable-stream": "1.0.34", + "split-ca": "1.0.1" }, "dependencies": { - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "underscore.string": { - "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", - "dev": true + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + } } } }, - "grunt-legacy-util": { - "version": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", - "dev": true, + "dockerode": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz", + "integrity": "sha512-LQKXR5jyI+G/+5OhZCi40m0ArY4j46g7Tl71Vtn10Ekt5TiyDzZAoqXOCS6edQpEuGbdFgSDJxleFqLxACpKJg==", "requires": { - "async": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "exit": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "getobject": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "hooker": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "underscore.string": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "which": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" - }, - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", - "dev": true - }, - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", - "dev": true - } + "concat-stream": "1.5.2", + "docker-modem": "1.0.4", + "tar-fs": "1.12.0" } }, - "grunt-mkdir": { - "version": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz", - "integrity": "sha1-c+GiasJKCFljY/TdlUsNMkheWOk=" + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + } }, - "grunt-mocha-test": { - "version": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", - "integrity": "sha1-emGEuYhg0Phb3qrWvqob199bvus=", - "dev": true, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "express": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", + "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", "requires": { - "mocha": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz" + "accepts": "1.3.4", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.0", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.2", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.1", + "serve-static": "1.13.1", + "setprototypeof": "1.1.0", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.1", + "vary": "1.1.2" }, "dependencies": { - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "dev": true, + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + "mime-types": "2.1.17", + "negotiator": "0.6.1" } }, - "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", - "dev": true + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "dev": true, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { - "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + "ms": "2.0.0" } }, - "mocha": { - "version": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", - "integrity": "sha1-cT223FAAGRqdA1gZXQkIeQ7LYVc=", - "dev": true, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", "requires": { - "commander": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "diff": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "glob": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "growl": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "jade": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", + "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", + "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.5.2" + } + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "send": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", + "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", + "requires": { + "debug": "2.6.9", + "depd": "1.1.2", + "destroy": "1.0.4", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + } + }, + "serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.1" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" } } }, - "grunt-shell": { - "version": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", - "integrity": "sha1-5KbRuSkSd2/ZOimcX2zGTpUlNlw=", - "dev": true, - "requires": { - "chalk": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz" - } - }, - "har-schema": { - "version": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "har-schema": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" - } - }, - "has-color": { - "version": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", - "dev": true - }, - "has-unicode": { - "version": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "hawk": { - "version": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", - "requires": { - "boom": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "cryptiles": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "sntp": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz" - } - }, - "heapdump": { - "version": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.9.tgz", - "integrity": "sha1-A8dOsN9dZ74Jgug0KbqcnSs7f3g=" - }, - "hoek": { - "version": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=" - }, - "hooker": { - "version": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true - }, - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "fs-extra": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", + "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", "requires": { - "depd": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "setprototypeof": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + "graceful-fs": "3.0.11", + "jsonfile": "2.4.0", + "rimraf": "2.6.2" }, "dependencies": { - "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - } - } - }, - "http-signature": { - "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "jsprim": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "sshpk": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz" - } - }, - "iconv-lite": { - "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=" - }, - "inflection": { - "version": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" - }, - "inflight": { - "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - } - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" - }, - "ipaddr.js": { - "version": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" - }, - "is-fullwidth-code-point": { - "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "requires": { + "natives": "1.1.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "4.1.11" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "optional": true + } + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "natives": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.1.tgz", + "integrity": "sha512-8eRaxn8u/4wN8tGkhlc2cgwwvOLMLUMUn4IYTexMgWd+LyUDfeXVkk2ygQR0hvIHbJQXgHujia3ieUUDwNGkEA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } } }, - "is-typedarray": { - "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "isstream": { - "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jade": { - "version": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, "requires": { - "commander": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + "async": "0.1.22", + "coffee-script": "1.3.3", + "colors": "0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "0.4.14", + "exit": "0.1.2", + "findup-sync": "0.1.3", + "getobject": "0.1.0", + "glob": "3.1.21", + "grunt-legacy-log": "0.1.3", + "grunt-legacy-util": "0.2.0", + "hooker": "0.2.3", + "iconv-lite": "0.2.11", + "js-yaml": "2.0.5", + "lodash": "0.9.2", + "minimatch": "0.2.14", + "nopt": "1.0.10", + "rimraf": "2.2.8", + "underscore.string": "2.2.1", + "which": "1.0.9" }, "dependencies": { - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "1.7.0", + "underscore.string": "2.4.0" + }, + "dependencies": { + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "requires": { + "glob": "3.2.11", + "lodash": "2.4.2" + }, + "dependencies": { + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimatch": "0.3.0" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + }, + "dependencies": { + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "0.6.2", + "grunt-legacy-log-utils": "0.1.1", + "hooker": "0.2.3", + "lodash": "2.4.2", + "underscore.string": "2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "0.6.2", + "lodash": "2.4.2", + "underscore.string": "2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "0.1.22", + "exit": "0.1.2", + "getobject": "0.1.0", + "hooker": "0.2.3", + "lodash": "0.9.2", + "underscore.string": "2.2.1", + "which": "1.0.9" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "0.1.16", + "esprima": "1.0.4" + } + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true } } }, - "js-yaml": { - "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", - "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "grunt-bunyan": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", + "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", "dev": true, "requires": { - "argparse": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "esprima": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" + "lodash": "2.4.2" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } } }, - "jsbn": { - "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stable-stringify": { - "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "grunt-contrib-clean": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", + "integrity": "sha1-9T397ghJsce0Dp67umn0jExgecU=", + "dev": true, "requires": { - "jsonify": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" + "rimraf": "2.2.8" + }, + "dependencies": { + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "grunt-contrib-coffee": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", + "integrity": "sha1-ixIme3TnM4sfKcW4txj7n4mYLxM=", + "dev": true, + "requires": { + "coffee-script": "1.6.3" + }, + "dependencies": { + "coffee-script": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", + "integrity": "sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4=", + "dev": true + } } }, - "json-stringify-safe": { - "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "grunt-execute": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", + "integrity": "sha1-yX64lDYS/vu3L749Mu+VIzxfouk=", + "dev": true + }, + "grunt-mkdir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz", + "integrity": "sha1-c+GiasJKCFljY/TdlUsNMkheWOk=" }, - "jsonfile": { - "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "grunt-mocha-test": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", + "integrity": "sha1-emGEuYhg0Phb3qrWvqob199bvus=", + "dev": true, "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" + "mocha": "1.14.0" }, "dependencies": { + "commander": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=", + "dev": true + }, + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", + "dev": true + }, + "glob": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "dev": true, + "requires": { + "graceful-fs": "2.0.3", + "inherits": "2.0.3", + "minimatch": "0.2.14" + } + }, "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "optional": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "dev": true + }, + "growl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "mocha": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", + "integrity": "sha1-cT223FAAGRqdA1gZXQkIeQ7LYVc=", + "dev": true, + "requires": { + "commander": "2.0.0", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.0", + "jade": "0.26.3", + "mkdirp": "0.3.5" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + } + } + }, + "grunt-shell": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", + "integrity": "sha1-5KbRuSkSd2/ZOimcX2zGTpUlNlw=", + "dev": true, + "requires": { + "chalk": "0.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", + "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", + "dev": true + }, + "chalk": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", + "integrity": "sha1-HJhDdzfxGZ68wdTEj9Qbn5yOjyM=", + "dev": true, + "requires": { + "ansi-styles": "0.2.0", + "has-color": "0.1.7" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true } } }, - "jsonify": { - "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "heapdump": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.9.tgz", + "integrity": "sha1-A8dOsN9dZ74Jgug0KbqcnSs7f3g=" + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "jsonparse": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=" }, - "jsprim": { - "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "extsprintf": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "json-schema": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "verror": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" - } - }, "lockfile": { - "version": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.3.tgz", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.3.tgz", "integrity": "sha1-Jjj8OaAzHpysGgS3F5mTHJxQ33k=" }, - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - }, "logger-sharelatex": { "version": "git+https://github.com/sharelatex/logger-sharelatex.git#9ee7b52eb2bbd8fcbb1e2c708587c1e93fd4c733", - "integrity": "sha1-aRMA+7GVHSmsRMbyj5cLhnueM9Q=", "requires": { "bunyan": "1.5.1", "coffee-script": "1.4.0", - "raven": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz" + "raven": "1.2.1" }, "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, "bunyan": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", "requires": { "dtrace-provider": "0.6.0", - "mv": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "mv": "2.1.1", "safe-json-stringify": "1.0.4" } }, - "coffee-script": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", - "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" - }, - "dtrace-provider": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", - "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", - "optional": true, + "coffee-script": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", + "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "dtrace-provider": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", + "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", + "optional": true, + "requires": { + "nan": "2.8.0" + } + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "lsmod": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", + "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" + } + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "optional": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "optional": true + }, + "raven": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", + "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", + "requires": { + "cookie": "0.3.1", + "json-stringify-safe": "5.0.1", + "lsmod": "1.0.0", + "stack-trace": "0.0.9", + "uuid": "3.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "6.0.4" + } + }, + "stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" + }, + "uuid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", + "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, + "lynx": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", + "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", + "requires": { + "mersenne": "0.0.4", + "statsd-parser": "0.0.4" + }, + "dependencies": { + "mersenne": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" + }, + "statsd-parser": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", + "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" + } + } + }, + "metrics-sharelatex": { + "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#05bd57604115ae5efdca2a419fbef4f25e9a780f", + "requires": { + "coffee-script": "1.6.0", + "lynx": "0.1.1", + "underscore": "1.6.0" + }, + "dependencies": { + "coffee-script": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + }, + "lynx": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", + "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", + "requires": { + "mersenne": "0.0.4", + "statsd-parser": "0.0.4" + } + }, + "mersenne": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" + }, + "statsd-parser": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", + "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" + }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" + } + } + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" + }, + "mocha": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mysql": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", + "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", + "requires": { + "bignumber.js": "2.0.7", + "readable-stream": "1.1.14", + "require-all": "1.0.0" + }, + "dependencies": { + "bignumber.js": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", + "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "require-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", + "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "1.4.1", + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + } + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.17" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.1.0" + } + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.0" + } + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + } + } + }, + "safe-buffer": { + "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" + }, + "safe-json-stringify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", + "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", + "optional": true + }, + "sandboxed-module": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", + "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", + "dev": true, + "requires": { + "require-like": "0.1.2", + "stack-trace": "0.0.6" + }, + "dependencies": { + "require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", + "dev": true + }, + "stack-trace": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", + "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", + "dev": true + } + } + }, + "sequelize": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-2.1.3.tgz", + "integrity": "sha1-FrpROxNnlY/oIzhYEelRFFHt6tE=", + "requires": { + "bluebird": "2.9.34", + "dottie": "0.3.1", + "generic-pool": "2.2.0", + "inflection": "1.12.0", + "lodash": "3.10.1", + "moment": "2.20.1", + "node-uuid": "1.4.8", + "toposort-class": "0.3.1", + "validator": "3.43.0" + }, + "dependencies": { + "bluebird": { + "version": "2.9.34", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz", + "integrity": "sha1-L3tOyAIWMoqf3evfacjUlC/v99g=" + }, + "dottie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-0.3.1.tgz", + "integrity": "sha1-QYX3d0iZYKwIQTHeP5oUoXm7dJ8=" + }, + "generic-pool": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.2.0.tgz", + "integrity": "sha1-i0ZcGnWI6p3SuxM72gu2a/74pj4=" + }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" + }, + "toposort-class": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-0.3.1.tgz", + "integrity": "sha1-0TM67XNPhgjO0ShiGL0sRfYO/0g=" + }, + "validator": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz", + "integrity": "sha1-lkZLmS1BloM9l6GUv0Cxn/VLrgU=" + } + } + }, + "settings-sharelatex": { + "version": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", + "requires": { + "coffee-script": "1.6.0" + }, + "dependencies": { + "coffee-script": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + } + } + }, + "sinon": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", + "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", + "dev": true, + "requires": { + "buster-format": "0.5.6" + }, + "dependencies": { + "buster-core": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", + "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", + "dev": true + }, + "buster-format": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", + "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", + "dev": true, + "requires": { + "buster-core": "0.6.4" + } + } + } + }, + "smoke-test-sharelatex": { + "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", + "requires": { + "mocha": "1.17.1" + }, + "dependencies": { + "commander": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" + }, + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" + }, + "glob": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "requires": { + "graceful-fs": "2.0.3", + "inherits": "2.0.3", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" + }, + "growl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" + } + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "mocha": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", + "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", + "requires": { + "commander": "2.0.0", + "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.0", + "jade": "0.26.3", + "mkdirp": "0.3.5" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + } + } + }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, + "sqlite3": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-3.1.13.tgz", + "integrity": "sha512-JxXKPJnkZ6NuHRojq+g2WXWBt3M1G9sjZaYiHEWSTGijDM3cwju/0T2XbWqMXFmPqDgw+iB7zKQvnns4bvzXlw==", + "requires": { + "nan": "2.7.0", + "node-pre-gyp": "0.6.38" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.8", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.4", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.30.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.17", + "bundled": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, + "node-pre-gyp": { + "version": "0.6.38", + "bundled": true, + "requires": { + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, "requires": { - "nan": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz" + "abbrev": "1.1.1", + "osenv": "0.1.4" } - } - } - }, - "lru-cache": { - "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "lsmod": { - "version": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" - }, - "lynx": { - "version": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", - "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", - "requires": { - "mersenne": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "statsd-parser": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz" - } - }, - "media-typer": { - "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "mersenne": { - "version": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" - }, - "methods": { - "version": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "metrics-sharelatex": { - "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#05bd57604115ae5efdca2a419fbef4f25e9a780f", - "integrity": "sha1-ruLAc3Tl1GZrAQjP/K4NeRajdW4=", - "requires": { - "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "lynx": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", - "underscore": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" - }, - "dependencies": { - "coffee-script": { - "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" }, - "lynx": { - "version": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", - "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", + "npmlog": { + "version": "4.1.2", + "bundled": true, "requires": { - "mersenne": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "statsd-parser": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz" + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" } }, - "underscore": { - "version": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" - } - } - }, - "mime": { - "version": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=" - }, - "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz" - } - }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "requires": { - "brace-expansion": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz" - } - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" - }, - "mocha": { - "version": "https://registry.npmjs.org/mocha/-/mocha-1.10.0.tgz", - "integrity": "sha1-8WrMlQ75Vm+/kIvPttXQQWw50No=", - "dev": true, - "requires": { - "commander": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "diff": "https://registry.npmjs.org/diff/-/diff-1.0.2.tgz", - "glob": "https://registry.npmjs.org/glob/-/glob-3.2.1.tgz", - "growl": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "jade": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.3.tgz", - "ms": "https://registry.npmjs.org/ms/-/ms-0.3.0.tgz" - }, - "dependencies": { - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", - "dev": true + "number-is-nan": { + "version": "1.0.1", + "bundled": true }, - "diff": { - "version": "https://registry.npmjs.org/diff/-/diff-1.0.2.tgz", - "integrity": "sha1-Suc/Gu6Nb89ITxoc53zmUdm38Mk=", - "dev": true + "oauth-sign": { + "version": "0.8.2", + "bundled": true }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-3.2.1.tgz", - "integrity": "sha1-V69w7HO6IyO/4/KaBndl22TF11g=", - "dev": true, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + "wrappy": "1.0.2" } }, - "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", - "dev": true + "os-homedir": { + "version": "1.0.2", + "bundled": true }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", - "dev": true + "os-tmpdir": { + "version": "1.0.2", + "bundled": true }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "dev": true, + "osenv": { + "version": "0.1.4", + "bundled": true, "requires": { - "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" } }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.3.tgz", - "integrity": "sha1-WV4lHBNww6aLqyE20ONIuBBa3xM=", - "dev": true + "path-is-absolute": { + "version": "1.0.1", + "bundled": true }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.3.0.tgz", - "integrity": "sha1-A+3DSNYT5mpWSGz9rFO8vomcvWE=", - "dev": true - } - } - }, - "moment": { - "version": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", - "integrity": "sha1-1usaRsvMFKKy+UNBEsH/iQfzE/0=" - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mv": { - "version": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "optional": true, - "requires": { - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "ncp": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz" - }, - "dependencies": { - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "optional": true, + "performance-now": { + "version": "0.2.0", + "bundled": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qs": { + "version": "6.4.0", + "bundled": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, "requires": { - "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } } }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, + "readable-stream": { + "version": "2.3.3", + "bundled": true, "requires": { - "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" } }, - "rimraf": { - "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "optional": true, + "request": { + "version": "2.81.0", + "bundled": true, "requires": { - "glob": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz" + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" } - } - } - }, - "mysql": { - "version": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", - "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", - "requires": { - "bignumber.js": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", - "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "require-all": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz" - } - }, - "nan": { - "version": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" - }, - "natives": { - "version": "https://registry.npmjs.org/natives/-/natives-1.1.1.tgz", - "integrity": "sha1-ARrM4ffL2H97prMJPWzZOSvhxXQ=" - }, - "ncp": { - "version": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "optional": true - }, - "negotiator": { - "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "node-pre-gyp": { - "version": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha1-wA6WhgsjwOFCCse+/FBE4deNhkk=", - "requires": { - "detect-libc": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "hawk": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "nopt": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "npmlog": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "rc": "https://registry.npmjs.org/rc/-/rc-1.2.3.tgz", - "request": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "semver": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "tar": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "tar-pack": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz" - }, - "dependencies": { - "ajv": { - "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, "requires": { - "co": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "json-stable-stringify": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" + "glob": "7.1.2" } }, - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + "safe-buffer": { + "version": "5.1.1", + "bundled": true }, - "aws-sign2": { - "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + "semver": { + "version": "5.4.1", + "bundled": true }, - "boom": { - "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, "requires": { - "hoek": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + "hoek": "2.16.3" } }, - "cryptiles": { - "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "sshpk": { + "version": "1.13.1", + "bundled": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "bundled": true, "requires": { - "boom": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + "safe-buffer": "5.1.1" } }, - "form-data": { - "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "stringstream": { + "version": "0.0.5", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, "requires": { - "asynckit": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz" + "ansi-regex": "2.1.1" } }, - "har-schema": { - "version": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + "strip-json-comments": { + "version": "2.0.1", + "bundled": true }, - "har-validator": { - "version": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "tar": { + "version": "2.2.1", + "bundled": true, "requires": { - "ajv": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "har-schema": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz" + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" } }, - "hawk": { - "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "tar-pack": { + "version": "3.4.0", + "bundled": true, "requires": { - "boom": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "cryptiles": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "hoek": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "sntp": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" } }, - "hoek": { - "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "http-signature": { - "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "tough-cookie": { + "version": "2.3.3", + "bundled": true, "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "jsprim": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "sshpk": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz" + "punycode": "1.4.1" } }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, "requires": { - "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "safe-buffer": "5.1.1" } }, - "performance-now": { - "version": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true }, - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + "uid-number": { + "version": "0.0.6", + "bundled": true }, - "request": { - "version": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.1.0", + "bundled": true + }, + "verror": { + "version": "1.10.0", + "bundled": true, "requires": { - "aws-sign2": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "caseless": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "form-data": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "har-validator": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "hawk": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "http-signature": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "performance-now": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "tunnel-agent": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "uuid": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz" + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } } }, - "sntp": { - "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "wide-align": { + "version": "1.1.2", + "bundled": true, "requires": { - "hoek": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + "string-width": "1.0.2" } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true } } }, - "nopt": { - "version": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "osenv": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz" - } - }, - "npmlog": { - "version": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", - "requires": { - "are-we-there-yet": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "console-control-strings": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "gauge": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "set-blocking": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - } - }, - "number-is-nan": { - "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "on-finished": { - "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - } - }, - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - } - }, - "os-homedir": { - "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "requires": { - "os-homedir": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" - } - }, - "parseurl": { - "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-is-absolute": { - "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-to-regexp": { - "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "performance-now": { - "version": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "process-nextick-args": { - "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "proxy-addr": { - "version": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", - "requires": { - "forwarded": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "ipaddr.js": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz" - } - }, - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "requires": { - "end-of-stream": "1.4.1", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - } - }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" - }, - "range-parser": { - "version": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raven": { - "version": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", - "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", - "requires": { - "cookie": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "lsmod": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "stack-trace": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "uuid": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz" - } - }, - "raw-body": { - "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - } - }, - "rc": { - "version": "https://registry.npmjs.org/rc/-/rc-1.2.3.tgz", - "integrity": "sha1-UVdakA+N1oOBxxC0cSwhVMPiA1s=", - "requires": { - "deep-extend": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "ini": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "minimist": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "strip-json-comments": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - }, - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - } - }, - "request": { - "version": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", - "requires": { - "aws-sign2": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "caseless": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "form-data": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "har-validator": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "hawk": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "http-signature": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "performance-now": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "qs": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "tunnel-agent": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "uuid": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz" - }, - "dependencies": { - "uuid": { - "version": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" - } - } - }, - "require-all": { - "version": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", - "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" - }, - "require-like": { - "version": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", - "dev": true - }, - "rimraf": { - "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", - "requires": { - "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" - } - }, - "safe-buffer": { - "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" - }, - "safe-json-stringify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", - "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", - "optional": true + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, - "sandboxed-module": { - "version": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", - "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", "dev": true, "requires": { - "require-like": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "stack-trace": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz" - }, - "dependencies": { - "stack-trace": { - "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", - "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", - "dev": true - } - } - }, - "semver": { - "version": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" - }, - "send": { - "version": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "integrity": "sha1-pw4coh0TgsEdDZ9iMd6ygQgNerM=", - "requires": { - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "depd": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "destroy": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "etag": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "mime": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" - }, - "dependencies": { - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - } - } - }, - "sequelize": { - "version": "https://registry.npmjs.org/sequelize/-/sequelize-2.1.3.tgz", - "integrity": "sha1-FrpROxNnlY/oIzhYEelRFFHt6tE=", - "requires": { - "bluebird": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz", - "dottie": "https://registry.npmjs.org/dottie/-/dottie-0.3.1.tgz", - "generic-pool": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.2.0.tgz", - "inflection": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "moment": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", - "node-uuid": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "toposort-class": "https://registry.npmjs.org/toposort-class/-/toposort-class-0.3.1.tgz", - "validator": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz" - }, - "dependencies": { - "node-uuid": { - "version": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" - } - } - }, - "serve-static": { - "version": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha1-TFfVNASnYdjy58HooYpH2/J4pxk=", - "requires": { - "encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "send": "https://registry.npmjs.org/send/-/send-0.16.1.tgz" + "has-flag": "2.0.0" } }, - "set-blocking": { - "version": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "settings-sharelatex": { - "version": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", - "integrity": "sha1-RatFGqtZ7wuhO78vGSB60S3H9Rc=", + "tar-fs": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", + "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", "requires": { - "coffee-script": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" + "mkdirp": "0.5.1", + "pump": "1.0.3", + "tar-stream": "1.5.5" }, "dependencies": { - "coffee-script": { - "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } } } }, - "sigmund": { - "version": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "signal-exit": { - "version": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "sinon": { - "version": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", - "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", - "dev": true, - "requires": { - "buster-format": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz" - } - }, - "smoke-test-sharelatex": { - "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", - "integrity": "sha1-s6idlk0vuV2Kz+u6rn3ITbaMEQ4=", + "tar-stream": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", + "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", "requires": { - "mocha": "1.17.1" + "bl": "1.2.1", + "end-of-stream": "1.4.1", + "readable-stream": "2.3.3", + "xtend": "4.0.1" }, - "dependencies": { - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "requires": { - "graceful-fs": "2.0.3", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "0.2.14" - } - }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "requires": { - "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "sigmund": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "1.0.0", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "string_decoder": "1.0.3", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" } }, - "mocha": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", - "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", "requires": { - "commander": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "diff": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "glob": "3.2.3", - "growl": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "jade": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" } } } }, - "sntp": { - "version": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", - "requires": { - "hoek": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz" - } + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, - "split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + "timekeeper": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", + "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", + "dev": true }, - "sqlite3": { - "version": "https://registry.npmjs.org/sqlite3/-/sqlite3-3.1.13.tgz", - "integrity": "sha1-2ZCgVic5J2jeYni6/Rox/f6Qfdk=", + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "v8-profiler": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", + "integrity": "sha1-6DgcvrtbX9DKjSsJ9qAYGhWNs00=", "requires": { - "nan": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", - "node-pre-gyp": "0.6.38" + "nan": "2.8.0", + "node-pre-gyp": "0.6.39" }, "dependencies": { "abbrev": { @@ -2076,7 +3462,7 @@ "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "requires": { "delegates": "1.0.0", - "readable-stream": "2.3.3" + "readable-stream": "2.3.4" } }, "asn1": { @@ -2118,14 +3504,6 @@ "tweetnacl": "0.14.5" } }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "2.0.3" - } - }, "boom": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", @@ -2135,9 +3513,9 @@ } }, "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -2159,9 +3537,9 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { "delayed-stream": "1.0.0" } @@ -2227,6 +3605,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", @@ -2257,7 +3640,7 @@ "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", "requires": { "asynckit": "0.4.0", - "combined-stream": "1.0.5", + "combined-stream": "1.0.6", "mime-types": "2.1.17" } }, @@ -2395,9 +3778,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -2487,7 +3870,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.8" + "brace-expansion": "1.1.11" } }, "minimist": { @@ -2509,24 +3892,26 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nan": { - "version": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", - "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" }, "node-pre-gyp": { - "version": "0.6.38", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz", - "integrity": "sha1-6Sog+DQWQVu0CG9tH7eLPac9ET0=", + "version": "0.6.39", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", + "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", "requires": { + "detect-libc": "1.0.3", "hawk": "3.1.3", "mkdirp": "0.5.1", "nopt": "4.0.1", "npmlog": "4.1.2", - "rc": "1.2.1", + "rc": "1.2.5", "request": "2.81.0", "rimraf": "2.6.2", - "semver": "5.4.1", + "semver": "5.5.0", "tar": "2.2.1", - "tar-pack": "3.4.0" + "tar-pack": "3.4.1" } }, "nopt": { @@ -2602,9 +3987,9 @@ "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" }, "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "punycode": { "version": "1.4.1", @@ -2617,12 +4002,12 @@ "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" }, "rc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", - "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz", + "integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=", "requires": { "deep-extend": "0.4.2", - "ini": "1.3.4", + "ini": "1.3.5", "minimist": "1.2.0", "strip-json-comments": "2.0.1" }, @@ -2635,14 +4020,14 @@ } }, "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz", + "integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", "isarray": "1.0.0", - "process-nextick-args": "1.0.7", + "process-nextick-args": "2.0.0", "safe-buffer": "5.1.1", "string_decoder": "1.0.3", "util-deprecate": "1.0.2" @@ -2656,7 +4041,7 @@ "aws-sign2": "0.6.0", "aws4": "1.6.0", "caseless": "0.12.0", - "combined-stream": "1.0.5", + "combined-stream": "1.0.6", "extend": "3.0.1", "forever-agent": "0.6.1", "form-data": "2.1.4", @@ -2674,7 +4059,7 @@ "stringstream": "0.0.5", "tough-cookie": "2.3.3", "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "uuid": "3.2.1" } }, "rimraf": { @@ -2691,9 +4076,9 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "set-blocking": { "version": "2.0.0", @@ -2776,21 +4161,21 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", "requires": { - "block-stream": "0.0.9", + "block-stream": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "fstream": "1.0.11", "inherits": "2.0.3" } }, "tar-pack": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", - "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", "requires": { "debug": "2.6.9", "fstream": "1.0.11", "fstream-ignore": "1.0.5", "once": "1.4.0", - "readable-stream": "2.3.3", + "readable-stream": "2.3.4", "rimraf": "2.6.2", "tar": "2.2.1", "uid-number": "0.0.6" @@ -2829,9 +4214,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, "verror": { "version": "1.10.0", @@ -2865,292 +4250,13 @@ } } }, - "sshpk": { - "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "bcrypt-pbkdf": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "dashdash": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "ecc-jsbn": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "getpass": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "jsbn": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "tweetnacl": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - } - }, - "stack-trace": { - "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - }, - "statsd-parser": { - "version": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", - "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" - }, - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" - }, - "string-width": { - "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - } - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "stringstream": { - "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - } - }, - "strip-json-comments": { - "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "tar": { - "version": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "fstream": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - } - }, - "tar-fs": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", - "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", - "requires": { - "mkdirp": "0.5.1", - "pump": "1.0.3", - "tar-stream": "1.5.5" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - } - } - } - }, - "tar-pack": { - "version": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha1-4dvAOpudO6B+iWrQJzF+tnmhCh8=", - "requires": { - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "fstream": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "fstream-ignore": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "tar": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "uid-number": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" - }, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", - "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - } - } - } - }, - "tar-stream": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", - "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", - "requires": { - "bl": "1.2.1", - "end-of-stream": "1.4.1", - "readable-stream": "2.3.3", - "xtend": "4.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "1.0.0", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "string_decoder": "1.0.3", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - } - } - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "timekeeper": { - "version": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", - "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", - "dev": true - }, - "toposort-class": { - "version": "https://registry.npmjs.org/toposort-class/-/toposort-class-0.3.1.tgz", - "integrity": "sha1-0TM67XNPhgjO0ShiGL0sRfYO/0g=" - }, - "tough-cookie": { - "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - } - }, - "tunnel-agent": { - "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - } - }, - "tweetnacl": { - "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-detect": { - "version": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - }, - "type-is": { - "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "uid-number": { - "version": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "underscore": { - "version": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" - }, - "underscore.string": { - "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", - "dev": true - }, - "unpipe": { - "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "util-deprecate": { - "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" - }, - "v8-profiler": { - "version": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", - "integrity": "sha1-6DgcvrtbX9DKjSsJ9qAYGhWNs00=", - "requires": { - "nan": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "node-pre-gyp": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz" - } - }, - "validator": { - "version": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz", - "integrity": "sha1-lkZLmS1BloM9l6GUv0Cxn/VLrgU=" - }, - "vary": { - "version": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "extsprintf": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" - } - }, - "which": { - "version": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", - "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", - "dev": true - }, - "wide-align": { - "version": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", - "requires": { - "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" - } - }, "wrappy": { "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "wrench": { - "version": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" }, "xtend": { diff --git a/package.json b/package.json index efc7aa0d..e44f2e48 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,67 @@ { - "name": "node-clsi", - "description": "A Node.js implementation of the CLSI LaTeX web-API", - "version": "0.1.4", + "name": "node-clsi", + "description": "A Node.js implementation of the CLSI LaTeX web-API", + "version": "0.1.4", "repository": { - "type": "git", + "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" - }, + }, "scripts": { - "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3013/status) do sleep 1; done", - "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000", - "test:acceptance:dir": "npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", - "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", - "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", - "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", - "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", - "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", - "start": "npm run compile:app && node app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 $@ test/acceptance/js", - "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", - "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", - "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", - "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", - "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && compile:test:smoke", + "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3013/status) do sleep 1; done", + "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000", + "test:acceptance:dir": "npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", + "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", + "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", + "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", + "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", + "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", + "start": "npm run compile:app && node app.js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", + "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", + "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", + "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", "nodemon": "nodemon --config nodemon.json" - }, - "author": "James Allen <james@sharelatex.com>", + }, + "author": "James Allen <james@sharelatex.com>", "dependencies": { - "async": "0.2.9", - "body-parser": "^1.2.0", - "dockerode": "^2.5.3", - "express": "^4.2.0", - "fs-extra": "^0.16.3", - "grunt-mkdir": "^1.0.0", - "heapdump": "^0.3.5", - "lockfile": "^1.0.3", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", - "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", - "mkdirp": "0.3.5", - "mysql": "2.6.2", - "request": "^2.21.0", - "sequelize": "^2.1.3", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~3.1.8", - "underscore": "^1.8.2", - "v8-profiler": "^5.2.4", + "async": "0.2.9", + "body-parser": "^1.2.0", + "dockerode": "^2.5.3", + "express": "^4.2.0", + "fs-extra": "^0.16.3", + "grunt-mkdir": "^1.0.0", + "heapdump": "^0.3.5", + "lockfile": "^1.0.3", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", + "lynx": "0.0.11", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", + "mkdirp": "0.3.5", + "mysql": "2.6.2", + "request": "^2.21.0", + "sequelize": "^2.1.3", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", + "sqlite3": "~3.1.8", + "underscore": "^1.8.2", + "v8-profiler": "^5.2.4", "wrench": "~1.5.4" - }, + }, "devDependencies": { - "mocha": "1.10.0", - "coffee-script": "1.6.0", - "chai": "~1.8.1", - "sinon": "~1.7.3", - "grunt": "~0.4.2", - "grunt-contrib-coffee": "~0.7.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-shell": "~0.6.1", - "grunt-mocha-test": "~0.8.1", - "sandboxed-module": "~0.3.0", - "timekeeper": "0.0.4", - "grunt-execute": "^0.1.5", - "bunyan": "^0.22.1", + "mocha": "^4.0.1", + "coffee-script": "1.6.0", + "chai": "~1.8.1", + "sinon": "~1.7.3", + "grunt": "~0.4.2", + "grunt-contrib-coffee": "~0.7.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-shell": "~0.6.1", + "grunt-mocha-test": "~0.8.1", + "sandboxed-module": "~0.3.0", + "timekeeper": "0.0.4", + "grunt-execute": "^0.1.5", + "bunyan": "^0.22.1", "grunt-bunyan": "^0.5.0" } } diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 4d431c29..e9a58b56 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -11,7 +11,8 @@ try catch e convertToPng = (pdfPath, pngPath, callback = (error) ->) -> - convert = ChildProcess.exec "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" + command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" + convert = ChildProcess.exec command stdout = "" convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() diff --git a/test/acceptance/coffee/helpers/Client.coffee b/test/acceptance/coffee/helpers/Client.coffee index 76634966..546c2351 100644 --- a/test/acceptance/coffee/helpers/Client.coffee +++ b/test/acceptance/coffee/helpers/Client.coffee @@ -31,6 +31,7 @@ module.exports = Client = express = require("express") app = express() app.use express.static(directory) + console.log("starting test server on", port, host) app.listen(port, host).on "error", (error) -> console.error "error starting server:", error.message process.exit(1) From a2a8b70b744b451d27f71bea2acc036ce33f8afc Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Feb 2018 11:36:32 +0000 Subject: [PATCH 282/709] acceptence tests pass inside docker container (apart from sync) --- Dockerfile | 15 +- app.coffee | 12 +- config/settings.defaults.coffee | 1 - docker-compose.yml | 33 +- install_deps.sh | 4 + package-lock.json | 531 +++++++++--------- package.json | 2 +- .../coffee/BrokenLatexFileTests.coffee | 5 +- .../coffee/DeleteOldFilesTest.coffee | 6 +- .../coffee/ExampleDocumentTests.coffee | 5 +- .../coffee/SimpleLatexFileTests.coffee | 4 +- test/acceptance/coffee/SynctexTests.coffee | 4 +- test/acceptance/coffee/TimeoutTests.coffee | 5 +- test/acceptance/coffee/UrlCachingTests.coffee | 4 +- test/acceptance/coffee/WordcountTests.coffee | 4 +- test/acceptance/coffee/helpers/ClsiApp.coffee | 24 + 16 files changed, 350 insertions(+), 309 deletions(-) create mode 100755 install_deps.sh create mode 100644 test/acceptance/coffee/helpers/ClsiApp.coffee diff --git a/Dockerfile b/Dockerfile index 83e452d3..05be8464 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,16 @@ -FROM node:6.9.5 - -RUN wget -qO- https://get.docker.com/ | sh - -run apt-get install poppler-utils vim ghostscript --yes - -# run git build-essential --yes -# RUN git clone https://github.com/netblue30/firejail.git -# RUN cd firejail && ./configure && make && make install-strip -# run mkdir /data +FROM node:6.13.0 COPY ./ /app WORKDIR /app + RUN npm install +RUN [ -e ./install_deps.sh ] && ./install_deps.sh + RUN npm run compile -ENV SHARELATEX_CONFIG /app/config/settings.production.coffee ENV NODE_ENV production CMD ["node","/app/app.js"] diff --git a/app.coffee b/app.coffee index ba7d2255..527b17e9 100644 --- a/app.coffee +++ b/app.coffee @@ -165,8 +165,16 @@ app.use (error, req, res, next) -> logger.error {err: error, url: req.url}, "server error" res.sendStatus(error?.statusCode || 500) -app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) -> - logger.info "CLSI starting up, listening on #{host}:#{port}" +port = (Settings.internal?.clsi?.port or 3013) +host = (Settings.internal?.clsi?.host or "localhost") + +if !module.parent # Called directly + app.listen port, host, (error) -> + logger.info "CLSI starting up, listening on #{host}:#{port}" + +module.exports = app + + setInterval () -> ProjectPersistenceManager.clearExpiredProjects() diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 4835b4fe..9b1e4be9 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -20,7 +20,6 @@ module.exports = clsi: port: 3013 host: process.env["LISTEN_ADDRESS"] or "0.0.0.0" - apis: clsi: url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" diff --git a/docker-compose.yml b/docker-compose.yml index d09ea6e0..a272e753 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,36 +14,21 @@ services: entrypoint: npm run test:unit test_acceptance: - image: node:6.9.5 - volumes: - - .:/app - environment: - REDIS_HOST: redis - MONGO_HOST: mongo - depends_on: - - redis - - mongo - working_dir: /app - entrypoint: npm run test:acceptance - - redis: - image: redis - - mongo: - image: mongo:3.4 - - app: - image: gcr.io/henry-terraform-admin/clsi build: . environment: TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: root # Not ideal, but makes running in dev very simple - COMMAND_RUNNER: docker-runner-sharelatex SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + COMMAND_RUNNER: docker-runner-sharelatex COMPILES_HOST_DIR: $PWD/compiles volumes: - /var/run/docker.sock:/var/run/docker.sock - ./docker-runner:/app/node_modules/docker-runner-sharelatex - - /Users/henry/Projects/sharelatex-dev-environment/clsi/compiles:/app/compiles - ports: - - 3013:3013 \ No newline at end of file + - ./compiles:/app/compiles + entrypoint: npm run test:acceptance + + redis: + image: redis + + mongo: + image: mongo:3.4 diff --git a/install_deps.sh b/install_deps.sh new file mode 100755 index 00000000..49bdc5c9 --- /dev/null +++ b/install_deps.sh @@ -0,0 +1,4 @@ +/bin/sh +wget -qO- https://get.docker.com/ | sh +apt-get install poppler-utils vim ghostscript --yes +npm rebuild diff --git a/package-lock.json b/package-lock.json index 5f30ca54..a5b7847c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,12 @@ "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true + }, "bl": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", @@ -208,6 +214,16 @@ } } }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "optional": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, "browser-stdout": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", @@ -396,8 +412,18 @@ "coffee-script": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=", - "dev": true + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + }, + "commander": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "concat-stream": { "version": "1.5.2", @@ -429,6 +455,11 @@ } } }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, "core-util-is": { "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" @@ -440,10 +471,15 @@ "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" } }, + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" + }, "docker-modem": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz", - "integrity": "sha512-pkXB9p7KWagegOXm2NsbVDBluQQLCBJzX9uYJzVbL6CHwe4d2sSbcACJ4K8ISX1l1JUUmFSiwNkBKc1uTiU4MA==", + "integrity": "sha1-JEJUsiax1/YCJfgjlb2FGQz3Ves=", "requires": { "JSONStream": "0.10.0", "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -467,17 +503,26 @@ "dockerode": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz", - "integrity": "sha512-LQKXR5jyI+G/+5OhZCi40m0ArY4j46g7Tl71Vtn10Ekt5TiyDzZAoqXOCS6edQpEuGbdFgSDJxleFqLxACpKJg==", + "integrity": "sha1-Wsw8Bx96JuRDpbWM83U+8vPCvAQ=", "requires": { "concat-stream": "1.5.2", "docker-modem": "1.0.4", "tar-fs": "1.12.0" } }, + "dtrace-provider": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", + "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", + "optional": true, + "requires": { + "nan": "2.8.0" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", "requires": { "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" } @@ -773,7 +818,7 @@ "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" }, "statuses": { "version": "1.3.1", @@ -932,6 +977,52 @@ } } }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true + } + } + }, + "graceful-fs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" + }, + "growl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" + }, "grunt": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", @@ -1478,6 +1569,32 @@ "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.9.tgz", "integrity": "sha1-A8dOsN9dZ74Jgug0KbqcnSs7f3g=" }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, "inherits": { "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" @@ -1486,6 +1603,32 @@ "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" + } + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "jsonparse": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", @@ -1504,22 +1647,6 @@ "raven": "1.2.1" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, "bunyan": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", @@ -1534,165 +1661,19 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "optional": true - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "dtrace-provider": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", - "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", - "optional": true, - "requires": { - "nan": "2.8.0" - } - }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "optional": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "optional": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "lsmod": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "optional": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "optional": true, - "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" - } - }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", - "optional": true - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "optional": true - }, - "raven": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", - "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", - "requires": { - "cookie": "0.3.1", - "json-stringify-safe": "5.0.1", - "lsmod": "1.0.0", - "stack-trace": "0.0.9", - "uuid": "3.0.0" - } - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "optional": true, - "requires": { - "glob": "6.0.4" - } - }, - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - }, - "uuid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "lsmod": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", + "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" + }, "lynx": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", @@ -1714,6 +1695,11 @@ } } }, + "mersenne": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" + }, "metrics-sharelatex": { "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#05bd57604115ae5efdca2a419fbef4f25e9a780f", "requires": { @@ -1722,11 +1708,6 @@ "underscore": "1.6.0" }, "dependencies": { - "coffee-script": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" - }, "lynx": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", @@ -1736,16 +1717,6 @@ "statsd-parser": "0.0.4" } }, - "mersenne": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" - }, - "statsd-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", - "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" - }, "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", @@ -1753,6 +1724,15 @@ } } }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "optional": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, "minimist": { "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" @@ -1765,7 +1745,7 @@ "mocha": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", + "integrity": "sha1-fYbPvPNcuCnidUwy4XNV7AUzh5Q=", "dev": true, "requires": { "browser-stdout": "1.3.0", @@ -1799,7 +1779,7 @@ "commander": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=", "dev": true }, "concat-map": { @@ -1811,7 +1791,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -1820,7 +1800,7 @@ "diff": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU=", "dev": true }, "fs.realpath": { @@ -1846,7 +1826,7 @@ "growl": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "integrity": "sha1-GSa6kM8+3+KttJJ/WIC8IsZseQ8=", "dev": true }, "inflight": { @@ -1922,6 +1902,34 @@ "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, "mysql": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", @@ -1975,6 +1983,18 @@ } } }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "optional": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, "node-uuid": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", @@ -1987,6 +2007,12 @@ "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "optional": true + }, "process-nextick-args": { "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" @@ -1994,16 +2020,28 @@ "pump": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "integrity": "sha1-Xf6DEcM7v2/BgmH580cCxHwIqVQ=", "requires": { "end-of-stream": "1.4.1", "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" } }, + "raven": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", + "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", + "requires": { + "cookie": "0.3.1", + "json-stringify-safe": "5.0.1", + "lsmod": "1.0.0", + "stack-trace": "0.0.9", + "uuid": "3.0.0" + } + }, "request": { "version": "2.83.0", "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", "requires": { "aws-sign2": "0.7.0", "aws4": "1.6.0", @@ -2361,7 +2399,7 @@ "uuid": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + "integrity": "sha1-EsUou51Y0LkmXZovbw/ovhf/HxQ=" }, "verror": { "version": "1.10.0", @@ -2375,6 +2413,15 @@ } } }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "6.0.4" + } + }, "safe-buffer": { "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" @@ -2471,15 +2518,13 @@ "version": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", "requires": { "coffee-script": "1.6.0" - }, - "dependencies": { - "coffee-script": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" - } } }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, "sinon": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", @@ -2512,16 +2557,6 @@ "mocha": "1.17.1" }, "dependencies": { - "commander": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" - }, - "diff": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" - }, "glob": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", @@ -2532,47 +2567,11 @@ "minimatch": "0.2.14" } }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" - }, - "growl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" - }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" - } - } - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, "minimatch": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", @@ -2595,11 +2594,6 @@ "jade": "0.26.3", "mkdirp": "0.3.5" } - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" } } }, @@ -3325,6 +3319,16 @@ } } }, + "stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" + }, + "statsd-parser": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", + "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" + }, "string_decoder": { "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" @@ -3332,7 +3336,7 @@ "supports-color": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "integrity": "sha1-iD992rwWUUKyphQn8zUt7RldGj4=", "dev": true, "requires": { "has-flag": "2.0.0" @@ -3361,7 +3365,7 @@ "tar-stream": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", - "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", + "integrity": "sha1-XK2Ed59FyDsfJQjZawnYjHIYr1U=", "requires": { "bl": "1.2.1", "end-of-stream": "1.4.1", @@ -3423,6 +3427,11 @@ "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", + "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + }, "v8-profiler": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", diff --git a/package.json b/package.json index e44f2e48..77e9423c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "sequelize": "^2.1.3", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "~3.1.8", + "sqlite3": "^3.1.13", "underscore": "^1.8.2", "v8-profiler": "^5.2.4", "wrench": "~1.5.4" diff --git a/test/acceptance/coffee/BrokenLatexFileTests.coffee b/test/acceptance/coffee/BrokenLatexFileTests.coffee index 5a92d5fc..a54df556 100644 --- a/test/acceptance/coffee/BrokenLatexFileTests.coffee +++ b/test/acceptance/coffee/BrokenLatexFileTests.coffee @@ -1,9 +1,10 @@ Client = require "./helpers/Client" request = require "request" require("chai").should() +ClsiApp = require "./helpers/ClsiApp" describe "Broken LaTeX file", -> - before -> + before (done)-> @broken_request = resources: [ path: "main.tex" @@ -24,6 +25,7 @@ describe "Broken LaTeX file", -> \\end{document} ''' ] + ClsiApp.ensureRunning done describe "on first run", -> before (done) -> @@ -31,6 +33,7 @@ describe "Broken LaTeX file", -> Client.compile @project_id, @broken_request, (@error, @res, @body) => done() it "should return a failure status", -> + console.log(@error, @res, @body) @body.compile.status.should.equal "failure" describe "on second run", -> diff --git a/test/acceptance/coffee/DeleteOldFilesTest.coffee b/test/acceptance/coffee/DeleteOldFilesTest.coffee index b8a1ff37..1cb67765 100644 --- a/test/acceptance/coffee/DeleteOldFilesTest.coffee +++ b/test/acceptance/coffee/DeleteOldFilesTest.coffee @@ -1,9 +1,10 @@ Client = require "./helpers/Client" request = require "request" require("chai").should() +ClsiApp = require "./helpers/ClsiApp" describe "Deleting Old Files", -> - before -> + before (done)-> @request = resources: [ path: "main.tex" @@ -14,7 +15,8 @@ describe "Deleting Old Files", -> \\end{document} ''' ] - + ClsiApp.ensureRunning done + describe "on first run", -> before (done) -> @project_id = Client.randomId() diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index e9a58b56..5ec61111 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -3,6 +3,7 @@ request = require "request" require("chai").should() fs = require "fs" ChildProcess = require "child_process" +ClsiApp = require "./helpers/ClsiApp" fixturePath = (path) -> __dirname + "/../fixtures/" + path @@ -86,7 +87,9 @@ Client.runServer(4242, fixturePath("examples")) describe "Example Documents", -> before (done) -> - ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> done() + ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> + ClsiApp.ensureRunning done + for example_dir in fs.readdirSync fixturePath("examples") do (example_dir) -> diff --git a/test/acceptance/coffee/SimpleLatexFileTests.coffee b/test/acceptance/coffee/SimpleLatexFileTests.coffee index 2693f63e..95b667ba 100644 --- a/test/acceptance/coffee/SimpleLatexFileTests.coffee +++ b/test/acceptance/coffee/SimpleLatexFileTests.coffee @@ -1,6 +1,7 @@ Client = require "./helpers/Client" request = require "request" require("chai").should() +ClsiApp = require "./helpers/ClsiApp" describe "Simple LaTeX file", -> before (done) -> @@ -15,7 +16,8 @@ describe "Simple LaTeX file", -> \\end{document} ''' ] - Client.compile @project_id, @request, (@error, @res, @body) => done() + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() it "should return the PDF", -> pdf = Client.getOutputFile(@body, "pdf") diff --git a/test/acceptance/coffee/SynctexTests.coffee b/test/acceptance/coffee/SynctexTests.coffee index 02b23978..c6adcf2b 100644 --- a/test/acceptance/coffee/SynctexTests.coffee +++ b/test/acceptance/coffee/SynctexTests.coffee @@ -2,6 +2,7 @@ Client = require "./helpers/Client" request = require "request" require("chai").should() expect = require("chai").expect +ClsiApp = require "./helpers/ClsiApp" describe "Syncing", -> before (done) -> @@ -16,7 +17,8 @@ describe "Syncing", -> ''' ] @project_id = Client.randomId() - Client.compile @project_id, @request, (@error, @res, @body) => done() + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() describe "from code to pdf", -> it "should return the correct location", (done) -> diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index 5e0058d3..38614871 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -1,6 +1,8 @@ Client = require "./helpers/Client" request = require "request" require("chai").should() +ClsiApp = require "./helpers/ClsiApp" + describe "Timed out compile", -> before (done) -> @@ -18,7 +20,8 @@ describe "Timed out compile", -> ''' ] @project_id = Client.randomId() - Client.compile @project_id, @request, (@error, @res, @body) => done() + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() it "should return a timeout error", -> @body.compile.error.should.equal "container timed out" diff --git a/test/acceptance/coffee/UrlCachingTests.coffee b/test/acceptance/coffee/UrlCachingTests.coffee index 9e6f3d62..cef74467 100644 --- a/test/acceptance/coffee/UrlCachingTests.coffee +++ b/test/acceptance/coffee/UrlCachingTests.coffee @@ -2,6 +2,7 @@ Client = require "./helpers/Client" request = require "request" require("chai").should() sinon = require "sinon" +ClsiApp = require "./helpers/ClsiApp" host = "localhost" @@ -46,7 +47,8 @@ describe "Url Caching", -> }] sinon.spy Server, "getFile" - Client.compile @project_id, @request, (@error, @res, @body) => done() + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() afterEach -> Server.getFile.restore() diff --git a/test/acceptance/coffee/WordcountTests.coffee b/test/acceptance/coffee/WordcountTests.coffee index d84ecba3..abace066 100644 --- a/test/acceptance/coffee/WordcountTests.coffee +++ b/test/acceptance/coffee/WordcountTests.coffee @@ -4,6 +4,7 @@ require("chai").should() expect = require("chai").expect path = require("path") fs = require("fs") +ClsiApp = require "./helpers/ClsiApp" describe "Syncing", -> before (done) -> @@ -13,7 +14,8 @@ describe "Syncing", -> content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") ] @project_id = Client.randomId() - Client.compile @project_id, @request, (@error, @res, @body) => done() + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() describe "wordcount file", -> it "should return wordcount info", (done) -> diff --git a/test/acceptance/coffee/helpers/ClsiApp.coffee b/test/acceptance/coffee/helpers/ClsiApp.coffee new file mode 100644 index 00000000..35be427b --- /dev/null +++ b/test/acceptance/coffee/helpers/ClsiApp.coffee @@ -0,0 +1,24 @@ +app = require('../../../../app') +require("logger-sharelatex").logger.level("error") +logger = require("logger-sharelatex") +Settings = require("settings-sharelatex") + +module.exports = + running: false + initing: false + callbacks: [] + ensureRunning: (callback = (error) ->) -> + if @running + return callback() + else if @initing + @callbacks.push callback + else + @initing = true + @callbacks.push callback + app.listen Settings.internal?.clsi?.port, "localhost", (error) => + throw error if error? + @running = true + logger.log("clsi running in dev mode") + + for callback in @callbacks + callback() \ No newline at end of file From a02adacc987c2acd19d2884ab507db51bc46819a Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Feb 2018 12:52:12 +0000 Subject: [PATCH 283/709] updated build sripts with 1.0.3 --- .nvmrc | 2 +- Dockerfile | 2 +- Makefile | 9 ++-- base.yml | 14 ++++++ docker-compose.ci.yml | 2 +- docker-compose.yml | 21 ++++---- kube.yaml | 41 ++++++++++++++++ nodemon.json | 1 + package.json | 112 +++++++++++++++++++++--------------------- 9 files changed, 131 insertions(+), 73 deletions(-) create mode 100644 base.yml create mode 100644 kube.yaml diff --git a/.nvmrc b/.nvmrc index e1e5d136..5917993c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.9.5 +6.13.0 diff --git a/Dockerfile b/Dockerfile index 05be8464..e195f969 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,13 @@ COPY ./ /app WORKDIR /app - RUN npm install RUN [ -e ./install_deps.sh ] && ./install_deps.sh RUN npm run compile +ENV SHARELATEX_CONFIG /app/config/settings.production.coffee ENV NODE_ENV production CMD ["node","/app/app.js"] diff --git a/Makefile b/Makefile index 6e7fca5f..828e8f82 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,16 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.0.1 +# Version: 1.0.3 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) PROJECT_NAME = clsi DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml -DOCKER_COMPOSE := docker-compose ${DOCKER_COMPOSE_FLAGS} +DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ + BRANCH_NAME=$(BRANCH_NAME) \ + PROJECT_NAME=$(PROJECT_NAME) \ + docker-compose ${DOCKER_COMPOSE_FLAGS} clean: rm -f app.js @@ -24,7 +27,7 @@ test_acceptance: test_clean # clear the database before each acceptance test run @[ -d test/acceptance ] && $(DOCKER_COMPOSE) run --rm test_acceptance -- ${MOCHA_ARGS} || echo "clsi has no acceptance tests" test_clean: - $(DOCKER_COMPOSE) down + $(DOCKER_COMPOSE) down -t 0 build: docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . diff --git a/base.yml b/base.yml new file mode 100644 index 00000000..ef5f44a1 --- /dev/null +++ b/base.yml @@ -0,0 +1,14 @@ +version: "2" + +services: + base: + environment: + TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 + TEXLIVE_IMAGE_USER: root + SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + COMMAND_RUNNER: docker-runner-sharelatex + COMPILES_HOST_DIR: $PWD/compiles + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./docker-runner:/app/node_modules/docker-runner-sharelatex + - ./compiles:/app/compiles diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index f34280d8..695ad3cf 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.0.1 +# Version: 1.0.3 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index a272e753..8c612e59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,13 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.0.1 +# Version: 1.0.3 version: "2" services: test_unit: - image: node:6.9.5 + image: node:6.13.0 volumes: - .:/app working_dir: /app @@ -15,16 +15,15 @@ services: test_acceptance: build: . + extends: + file: base.yml + service: base environment: - TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 - TEXLIVE_IMAGE_USER: root # Not ideal, but makes running in dev very simple - SHARELATEX_CONFIG: /app/config/settings.defaults.coffee - COMMAND_RUNNER: docker-runner-sharelatex - COMPILES_HOST_DIR: $PWD/compiles - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./docker-runner:/app/node_modules/docker-runner-sharelatex - - ./compiles:/app/compiles + REDIS_HOST: redis + MONGO_HOST: mongo + depends_on: + - redis + - mongo entrypoint: npm run test:acceptance redis: diff --git a/kube.yaml b/kube.yaml new file mode 100644 index 00000000..44a562d0 --- /dev/null +++ b/kube.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Service +metadata: + name: clsi + namespace: default +spec: + type: LoadBalancer + ports: + - port: 3009 + protocol: TCP + targetPort: 3009 + selector: + run: clsi +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: clsi + namespace: default +spec: + replicas: 2 + template: + metadata: + labels: + run: clsi + spec: + containers: + - name: clsi + image: gcr.io/henry-terraform-admin/clsi:6e0c79688030117a9c27d0fc01d1271e6ac3dd3e + imagePullPolicy: Always + readinessProbe: + httpGet: + path: status + port: 3009 + periodSeconds: 5 + initialDelaySeconds: 0 + failureThreshold: 3 + successThreshold: 1 + + + diff --git a/nodemon.json b/nodemon.json index 9044f921..9a3be8d9 100644 --- a/nodemon.json +++ b/nodemon.json @@ -4,6 +4,7 @@ "node_modules/" ], "verbose": true, + "legacyWatch": true, "execMap": { "js": "npm run start" }, diff --git a/package.json b/package.json index 77e9423c..c5d52c94 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,67 @@ { - "name": "node-clsi", - "description": "A Node.js implementation of the CLSI LaTeX web-API", - "version": "0.1.4", + "name": "node-clsi", + "description": "A Node.js implementation of the CLSI LaTeX web-API", + "version": "0.1.4", "repository": { - "type": "git", + "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" - }, + }, "scripts": { - "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3013/status) do sleep 1; done", - "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000", - "test:acceptance:dir": "npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", - "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", - "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", - "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", - "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", - "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", - "start": "npm run compile:app && node app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", - "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", - "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", - "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", - "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", - "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", + "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3013/status) do sleep 1; done", + "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000", + "test:acceptance:dir": "npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", + "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", + "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", + "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", + "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", + "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", + "start": "npm run compile:app && node app.js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", + "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", + "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", + "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", "nodemon": "nodemon --config nodemon.json" - }, - "author": "James Allen <james@sharelatex.com>", + }, + "author": "James Allen <james@sharelatex.com>", "dependencies": { - "async": "0.2.9", - "body-parser": "^1.2.0", - "dockerode": "^2.5.3", - "express": "^4.2.0", - "fs-extra": "^0.16.3", - "grunt-mkdir": "^1.0.0", - "heapdump": "^0.3.5", - "lockfile": "^1.0.3", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", - "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", - "mkdirp": "0.3.5", - "mysql": "2.6.2", - "request": "^2.21.0", - "sequelize": "^2.1.3", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "^3.1.13", - "underscore": "^1.8.2", - "v8-profiler": "^5.2.4", + "async": "0.2.9", + "body-parser": "^1.2.0", + "dockerode": "^2.5.3", + "express": "^4.2.0", + "fs-extra": "^0.16.3", + "grunt-mkdir": "^1.0.0", + "heapdump": "^0.3.5", + "lockfile": "^1.0.3", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", + "lynx": "0.0.11", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", + "mkdirp": "0.3.5", + "mysql": "2.6.2", + "request": "^2.21.0", + "sequelize": "^2.1.3", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", + "sqlite3": "^3.1.13", + "underscore": "^1.8.2", + "v8-profiler": "^5.2.4", "wrench": "~1.5.4" - }, + }, "devDependencies": { - "mocha": "^4.0.1", - "coffee-script": "1.6.0", - "chai": "~1.8.1", - "sinon": "~1.7.3", - "grunt": "~0.4.2", - "grunt-contrib-coffee": "~0.7.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-shell": "~0.6.1", - "grunt-mocha-test": "~0.8.1", - "sandboxed-module": "~0.3.0", - "timekeeper": "0.0.4", - "grunt-execute": "^0.1.5", - "bunyan": "^0.22.1", + "mocha": "^4.0.1", + "coffee-script": "1.6.0", + "chai": "~1.8.1", + "sinon": "~1.7.3", + "grunt": "~0.4.2", + "grunt-contrib-coffee": "~0.7.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-shell": "~0.6.1", + "grunt-mocha-test": "~0.8.1", + "sandboxed-module": "~0.3.0", + "timekeeper": "0.0.4", + "grunt-execute": "^0.1.5", + "bunyan": "^0.22.1", "grunt-bunyan": "^0.5.0" } } From 12b13d61997c78ce7188a29d6be7517d0cf62ad8 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Feb 2018 15:45:47 +0000 Subject: [PATCH 284/709] mount app as volume in docker container for local tests change to overrides --- base.yml => docker-compose-overrides.yml | 14 +++++++++++++- docker-compose.yml | 7 +++++-- 2 files changed, 18 insertions(+), 3 deletions(-) rename base.yml => docker-compose-overrides.yml (50%) diff --git a/base.yml b/docker-compose-overrides.yml similarity index 50% rename from base.yml rename to docker-compose-overrides.yml index ef5f44a1..3d64975f 100644 --- a/base.yml +++ b/docker-compose-overrides.yml @@ -1,7 +1,7 @@ version: "2" services: - base: + dev: environment: TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: root @@ -12,3 +12,15 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ./docker-runner:/app/node_modules/docker-runner-sharelatex - ./compiles:/app/compiles + + ci: + environment: + TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 + TEXLIVE_IMAGE_USER: root + SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + COMMAND_RUNNER: docker-runner-sharelatex + COMPILES_HOST_DIR: $PWD/compiles + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./docker-runner:/app/node_modules/docker-runner-sharelatex + - ./compiles:/app/compiles \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8c612e59..6126666c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,9 +15,12 @@ services: test_acceptance: build: . + volumes: + - .:/app + working_dir: /app extends: - file: base.yml - service: base + file: docker-compose-overrides.yml + service: dev environment: REDIS_HOST: redis MONGO_HOST: mongo From d698cc318fc7bf2e73e7d1b687487c145313d633 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 21 Feb 2018 15:42:51 +0000 Subject: [PATCH 285/709] updateded build scripts --- Jenkinsfile | 36 +++++++++++++++++-- Makefile | 7 +--- ...overrides.yml => docker-compose-config.yml | 0 docker-compose.ci.yml | 12 +++++-- docker-compose.yml | 6 ++-- kube.yaml | 8 ++--- 6 files changed, 50 insertions(+), 19 deletions(-) rename docker-compose-overrides.yml => docker-compose-config.yml (100%) diff --git a/Jenkinsfile b/Jenkinsfile index bc9ba014..326dce5f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,9 +9,34 @@ pipeline { } stages { - stage('Build') { + stage('Install') { + agent { + docker { + image 'node:6.13.0' + args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" + reuseNode true + } + } + steps { + // we need to disable logallrefupdates, else git clones + // during the npm install will require git to lookup the + // user id which does not exist in the container's + // /etc/passwd file, causing the clone to fail. + sh 'git config --global core.logallrefupdates false' + sh 'rm -rf node_modules' + sh 'npm install && npm rebuild' + } + } + + stage('Compile') { + agent { + docker { + image 'node:6.13.0' + reuseNode true + } + } steps { - sh 'make build' + sh 'npm run compile:all' } } @@ -29,7 +54,12 @@ pipeline { stage('Package and publish build') { steps { - sh 'make publish' + sh 'echo ${BUILD_NUMBER} > build_number.txt' + sh 'touch build.tar.gz' // Avoid tar warning about files changing during read + sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .' + withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { + s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") + } } } diff --git a/Makefile b/Makefile index 828e8f82..89066f54 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.0.3 +# Version: 1.1.0 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -28,10 +28,5 @@ test_acceptance: test_clean # clear the database before each acceptance test run test_clean: $(DOCKER_COMPOSE) down -t 0 -build: - docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . - -publish: - docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/docker-compose-overrides.yml b/docker-compose-config.yml similarity index 100% rename from docker-compose-overrides.yml rename to docker-compose-config.yml diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 695ad3cf..00740d4c 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,17 +1,23 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.0.3 +# Version: 1.1.0 version: "2" services: test_unit: - image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + image: node:6.13.0 + volumes: + - .:/app + working_dir: /app entrypoint: npm run test:unit:_run test_acceptance: - image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + image: node:6.13.0 + volumes: + - .:/app + working_dir: /app environment: REDIS_HOST: redis MONGO_HOST: mongo diff --git a/docker-compose.yml b/docker-compose.yml index 6126666c..c259abcd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.0.3 +# Version: 1.1.0 version: "2" @@ -14,12 +14,12 @@ services: entrypoint: npm run test:unit test_acceptance: - build: . + image: node:6.13.0 volumes: - .:/app working_dir: /app extends: - file: docker-compose-overrides.yml + file: docker-compose-config.yml service: dev environment: REDIS_HOST: redis diff --git a/kube.yaml b/kube.yaml index 44a562d0..d3fb0429 100644 --- a/kube.yaml +++ b/kube.yaml @@ -6,9 +6,9 @@ metadata: spec: type: LoadBalancer ports: - - port: 3009 + - port: 80 protocol: TCP - targetPort: 3009 + targetPort: 80 selector: run: clsi --- @@ -26,12 +26,12 @@ spec: spec: containers: - name: clsi - image: gcr.io/henry-terraform-admin/clsi:6e0c79688030117a9c27d0fc01d1271e6ac3dd3e + image: gcr.io/henry-terraform-admin/clsi imagePullPolicy: Always readinessProbe: httpGet: path: status - port: 3009 + port: 80 periodSeconds: 5 initialDelaySeconds: 0 failureThreshold: 3 From 8f6db5baff9e4f53fbf06b3053cb63448ddd6c60 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 26 Feb 2018 11:56:19 +0000 Subject: [PATCH 286/709] tests pass under app user --- Dockerfile | 23 +++++++++++++++++------ Jenkinsfile | 36 +++--------------------------------- Makefile | 5 +++++ docker-compose.ci.yml | 10 ++-------- docker-compose.yml | 2 +- install_deps.sh | 6 +++++- 6 files changed, 33 insertions(+), 49 deletions(-) diff --git a/Dockerfile b/Dockerfile index e195f969..881852da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:6.13.0 +FROM node:6.13.0 as app COPY ./ /app @@ -6,11 +6,22 @@ WORKDIR /app RUN npm install -RUN [ -e ./install_deps.sh ] && ./install_deps.sh -RUN npm run compile +RUN npm run compile:all + +FROM node:6.13.0 + +COPY --from=app /app /app -ENV SHARELATEX_CONFIG /app/config/settings.production.coffee -ENV NODE_ENV production +WORKDIR /app + + +# All app and node_modules will be owned by root. +# The app will run as the 'app' user, and so not have write permissions +# on any files it doesn't need. +RUN useradd --user-group --create-home --home-dir /app --shell /bin/bash app + +RUN [ -e ./install_deps.sh ] && ./install_deps.sh -CMD ["node","/app/app.js"] +USER app +CMD ["node","app.js"] diff --git a/Jenkinsfile b/Jenkinsfile index 326dce5f..bc9ba014 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,34 +9,9 @@ pipeline { } stages { - stage('Install') { - agent { - docker { - image 'node:6.13.0' - args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" - reuseNode true - } - } - steps { - // we need to disable logallrefupdates, else git clones - // during the npm install will require git to lookup the - // user id which does not exist in the container's - // /etc/passwd file, causing the clone to fail. - sh 'git config --global core.logallrefupdates false' - sh 'rm -rf node_modules' - sh 'npm install && npm rebuild' - } - } - - stage('Compile') { - agent { - docker { - image 'node:6.13.0' - reuseNode true - } - } + stage('Build') { steps { - sh 'npm run compile:all' + sh 'make build' } } @@ -54,12 +29,7 @@ pipeline { stage('Package and publish build') { steps { - sh 'echo ${BUILD_NUMBER} > build_number.txt' - sh 'touch build.tar.gz' // Avoid tar warning about files changing during read - sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .' - withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { - s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") - } + sh 'make publish' } } diff --git a/Makefile b/Makefile index 89066f54..b20654c6 100644 --- a/Makefile +++ b/Makefile @@ -28,5 +28,10 @@ test_acceptance: test_clean # clear the database before each acceptance test run test_clean: $(DOCKER_COMPOSE) down -t 0 +build: + docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . + +publish: + docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 00740d4c..cbe15fe9 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -7,17 +7,11 @@ version: "2" services: test_unit: - image: node:6.13.0 - volumes: - - .:/app - working_dir: /app + image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER entrypoint: npm run test:unit:_run test_acceptance: - image: node:6.13.0 - volumes: - - .:/app - working_dir: /app + image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER environment: REDIS_HOST: redis MONGO_HOST: mongo diff --git a/docker-compose.yml b/docker-compose.yml index c259abcd..cf907427 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: entrypoint: npm run test:unit test_acceptance: - image: node:6.13.0 + build: . volumes: - .:/app working_dir: /app diff --git a/install_deps.sh b/install_deps.sh index 49bdc5c9..3caa1c7c 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -1,4 +1,8 @@ /bin/sh wget -qO- https://get.docker.com/ | sh -apt-get install poppler-utils vim ghostscript --yes +apt-get install poppler-utils ghostscript --yes npm rebuild +usermod -aG docker app + +touch /var/run/docker.sock +chown root:docker /var/run/docker.sock From b8c22f4d74e56f03e6897c3cad9a90649e603e62 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 26 Feb 2018 14:29:30 +0000 Subject: [PATCH 287/709] wip, docker container is correctly created --- app/coffee/CompileManager.coffee | 40 +- config/settings.defaults.coffee | 2 - docker-compose.yml | 11 + .../coffee/BrokenLatexFileTests.coffee | 83 ++-- .../coffee/DeleteOldFilesTest.coffee | 60 +-- .../coffee/ExampleDocumentTests.coffee | 224 ++++----- .../coffee/SimpleLatexFileTests.coffee | 70 +-- test/acceptance/coffee/SynctexTests.coffee | 16 +- test/acceptance/coffee/TimeoutTests.coffee | 52 +-- test/acceptance/coffee/UrlCachingTests.coffee | 436 +++++++++--------- test/acceptance/coffee/WordcountTests.coffee | 72 +-- 11 files changed, 538 insertions(+), 528 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index d3d319af..a6df6dd5 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -41,7 +41,7 @@ module.exports = CompileManager = doCompile: (request, callback = (error, outputFiles) ->) -> compileDir = getCompileDir(request.project_id, request.user_id) - console.log("doCompile",compileDir ) + # console.log("doCompile", compileDir) timer = new Metrics.Timer("write-to-disk") logger.log project_id: request.project_id, user_id: request.user_id, "syncing resources to disk" @@ -206,9 +206,9 @@ module.exports = CompileManager = base_dir = Settings.path.synctexBaseDir(compileName) file_path = base_dir + "/" + file_name compileDir = getCompileDir(project_id, user_id) - synctex_path = Path.join(compileDir, "output.pdf") + synctex_path = "$COMPILE_DIR/output.pdf" command = ["code", synctex_path, file_path, line, column] - CompileManager._runSynctex command, (error, stdout) -> + CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> return callback(error) if error? if stdout.toLowerCase().indexOf("warning") == -1 logType = "log" @@ -221,9 +221,9 @@ module.exports = CompileManager = compileName = getCompileName(project_id, user_id) base_dir = Settings.path.synctexBaseDir(compileName) compileDir = getCompileDir(project_id, user_id) - synctex_path = Path.join(compileDir, "output.pdf") - logger.log({base_dir, project_id, synctex_path}, "base diiir") - CompileManager._runSynctex ["pdf", synctex_path, page, h, v], (error, stdout) -> + synctex_path = "$COMPILE_DIR/output.pdf" + command = ["pdf", synctex_path, page, h, v] + CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> return callback(error) if error? logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) @@ -242,20 +242,22 @@ module.exports = CompileManager = return callback(new Error("not a file")) if not stats?.isFile() callback() - _runSynctex: (args, callback = (error, stdout) ->) -> - bin_path = Path.resolve(__dirname + "/../../bin/synctex") + _runSynctex: (project_id, user_id, command, callback = (error, stdout) ->) -> seconds = 1000 - outputFilePath = args[1] - CompileManager._checkFileExists outputFilePath, (error) -> - return callback(error) if error? - if Settings.clsi?.synctexCommandWrapper? - [bin_path, args] = Settings.clsi?.synctexCommandWrapper bin_path, args - logger.log({bin_path, args}, "synctex being run") - 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) - callback(null, stdout) + + #this is a hack, only works for docker runner + command.unshift("/opt/synctex") + directory = getCompileDir(project_id, user_id) + timeout = 10 * 1000 + compileName = getCompileName(project_id, user_id) + console.log command, "_runSynctex" + + CommandRunner.run compileName, command, directory, Settings.clsi.docker.image, timeout, {}, (error, stdout) -> + console.log("synctex run", stdout) + if error? + logger.err err:error, command:command, "error running synctex" + return callback(error) + callback(null, stdout) _parseSynctexFromCodeOutput: (output) -> results = [] diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 9b1e4be9..d09784de 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -41,5 +41,3 @@ if process.env["COMMAND_RUNNER"] checkProjectsIntervalMs: 10 * 60 * 1000 module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] - -console.log module.exports \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index cf907427..090c3602 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,17 @@ services: - mongo entrypoint: npm run test:acceptance + synctex: + image: quay.io/sharelatex/texlive-full:2017.1 + volumes: + - ~/Projects/sharelatex-dev-environment/clsi/compiles/564c29f884179:/compile + - ./bin/synctex:/opt/synctex + + command: + /opt/synctex pdf /compile/output.pdf 1 100 200 + + + redis: image: redis diff --git a/test/acceptance/coffee/BrokenLatexFileTests.coffee b/test/acceptance/coffee/BrokenLatexFileTests.coffee index a54df556..755f28e7 100644 --- a/test/acceptance/coffee/BrokenLatexFileTests.coffee +++ b/test/acceptance/coffee/BrokenLatexFileTests.coffee @@ -1,49 +1,48 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +# Client = require "./helpers/Client" +# request = require "request" +# require("chai").should() +# ClsiApp = require "./helpers/ClsiApp" -describe "Broken LaTeX file", -> - before (done)-> - @broken_request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{articl % :( - \\begin{documen % :( - Broken - \\end{documen % :( - ''' - ] - @correct_request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' - ] - ClsiApp.ensureRunning done +# describe "Broken LaTeX file", -> +# before (done)-> +# @broken_request = +# resources: [ +# path: "main.tex" +# content: ''' +# \\documentclass{articl % :( +# \\begin{documen % :( +# Broken +# \\end{documen % :( +# ''' +# ] +# @correct_request = +# resources: [ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\begin{document} +# Hello world +# \\end{document} +# ''' +# ] +# ClsiApp.ensureRunning done - describe "on first run", -> - before (done) -> - @project_id = Client.randomId() - Client.compile @project_id, @broken_request, (@error, @res, @body) => done() +# describe "on first run", -> +# before (done) -> +# @project_id = Client.randomId() +# Client.compile @project_id, @broken_request, (@error, @res, @body) => done() - it "should return a failure status", -> - console.log(@error, @res, @body) - @body.compile.status.should.equal "failure" +# it "should return a failure status", -> +# @body.compile.status.should.equal "failure" - describe "on second run", -> - before (done) -> - @project_id = Client.randomId() - Client.compile @project_id, @correct_request, () => - Client.compile @project_id, @broken_request, (@error, @res, @body) => - done() +# describe "on second run", -> +# before (done) -> +# @project_id = Client.randomId() +# Client.compile @project_id, @correct_request, () => +# Client.compile @project_id, @broken_request, (@error, @res, @body) => +# done() - it "should return a failure status", -> - @body.compile.status.should.equal "failure" +# it "should return a failure status", -> +# @body.compile.status.should.equal "failure" diff --git a/test/acceptance/coffee/DeleteOldFilesTest.coffee b/test/acceptance/coffee/DeleteOldFilesTest.coffee index 1cb67765..750f5f93 100644 --- a/test/acceptance/coffee/DeleteOldFilesTest.coffee +++ b/test/acceptance/coffee/DeleteOldFilesTest.coffee @@ -1,36 +1,36 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +# Client = require "./helpers/Client" +# request = require "request" +# require("chai").should() +# ClsiApp = require "./helpers/ClsiApp" -describe "Deleting Old Files", -> - before (done)-> - @request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' - ] - ClsiApp.ensureRunning done +# describe "Deleting Old Files", -> +# before (done)-> +# @request = +# resources: [ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\begin{document} +# Hello world +# \\end{document} +# ''' +# ] +# ClsiApp.ensureRunning done - describe "on first run", -> - before (done) -> - @project_id = Client.randomId() - Client.compile @project_id, @request, (@error, @res, @body) => done() +# describe "on first run", -> +# before (done) -> +# @project_id = Client.randomId() +# Client.compile @project_id, @request, (@error, @res, @body) => done() - it "should return a success status", -> - @body.compile.status.should.equal "success" +# it "should return a success status", -> +# @body.compile.status.should.equal "success" - describe "after file has been deleted", -> - before (done) -> - @request.resources = [] - Client.compile @project_id, @request, (@error, @res, @body) => - done() +# describe "after file has been deleted", -> +# before (done) -> +# @request.resources = [] +# Client.compile @project_id, @request, (@error, @res, @body) => +# done() - it "should return a failure status", -> - @body.compile.status.should.equal "failure" +# it "should return a failure status", -> +# @body.compile.status.should.equal "failure" diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 5ec61111..1a1a0a0a 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -1,114 +1,114 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -fs = require "fs" -ChildProcess = require "child_process" -ClsiApp = require "./helpers/ClsiApp" - -fixturePath = (path) -> __dirname + "/../fixtures/" + path - -try - fs.mkdirSync(fixturePath("tmp")) -catch e - -convertToPng = (pdfPath, pngPath, callback = (error) ->) -> - command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" - convert = ChildProcess.exec command - stdout = "" - convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() - convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() - convert.on "exit", () -> - callback() - -compare = (originalPath, generatedPath, callback = (error, same) ->) -> - diff_file = "#{fixturePath(generatedPath)}-diff.png" - proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{diff_file}" - stderr = "" - proc.stderr.on "data", (chunk) -> stderr += chunk - proc.on "exit", () -> - if stderr.trim() == "0 (0)" - fs.unlink diff_file # remove output diff if test matches expected image - callback null, true - else - console.log "compare result", stderr - callback null, false - -checkPdfInfo = (pdfPath, callback = (error, output) ->) -> - proc = ChildProcess.exec "pdfinfo #{fixturePath(pdfPath)}" - stdout = "" - proc.stdout.on "data", (chunk) -> stdout += chunk - proc.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() - proc.on "exit", () -> - if stdout.match(/Optimized:\s+yes/) - callback null, true - else - console.log "pdfinfo result", stdout - callback null, false - -compareMultiplePages = (project_id, callback = (error) ->) -> - compareNext = (page_no, callback) -> - path = "tmp/#{project_id}-source-#{page_no}.png" - fs.stat fixturePath(path), (error, stat) -> - if error? - callback() - else - compare "tmp/#{project_id}-source-#{page_no}.png", "tmp/#{project_id}-generated-#{page_no}.png", (error, same) => - throw error if error? - same.should.equal true - compareNext page_no + 1, callback - compareNext 0, callback - -comparePdf = (project_id, example_dir, callback = (error) ->) -> - convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => - throw error if error? - convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => - throw error if error? - fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) => - if error? - compare "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) => - throw error if error? - same.should.equal true - callback() - else - compareMultiplePages project_id, (error) -> - throw error if error? - callback() - -downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) -> - writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf")) - request.get(url).pipe(writeStream) - writeStream.on "close", () => - checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) => - throw error if error? - optimised.should.equal true - comparePdf project_id, example_dir, callback - -Client.runServer(4242, fixturePath("examples")) - -describe "Example Documents", -> - before (done) -> - ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> - ClsiApp.ensureRunning done - - - for example_dir in fs.readdirSync fixturePath("examples") - do (example_dir) -> - describe example_dir, -> - before -> - @project_id = Client.randomId() + "_" + example_dir - - it "should generate the correct pdf", (done) -> - Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - if error || body?.compile?.status is "failure" - console.log "DEBUG: error", error, "body", JSON.stringify(body) - pdf = Client.getOutputFile body, "pdf" - downloadAndComparePdf(@project_id, example_dir, pdf.url, done) - - it "should generate the correct pdf on the second run as well", (done) -> - Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - if error || body?.compile?.status is "failure" - console.log "DEBUG: error", error, "body", JSON.stringify(body) - pdf = Client.getOutputFile body, "pdf" - downloadAndComparePdf(@project_id, example_dir, pdf.url, done) +# Client = require "./helpers/Client" +# request = require "request" +# require("chai").should() +# fs = require "fs" +# ChildProcess = require "child_process" +# ClsiApp = require "./helpers/ClsiApp" + +# fixturePath = (path) -> __dirname + "/../fixtures/" + path + +# try +# fs.mkdirSync(fixturePath("tmp")) +# catch e + +# convertToPng = (pdfPath, pngPath, callback = (error) ->) -> +# command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" +# convert = ChildProcess.exec command +# stdout = "" +# convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() +# convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() +# convert.on "exit", () -> +# callback() + +# compare = (originalPath, generatedPath, callback = (error, same) ->) -> +# diff_file = "#{fixturePath(generatedPath)}-diff.png" +# proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{diff_file}" +# stderr = "" +# proc.stderr.on "data", (chunk) -> stderr += chunk +# proc.on "exit", () -> +# if stderr.trim() == "0 (0)" +# fs.unlink diff_file # remove output diff if test matches expected image +# callback null, true +# else +# console.log "compare result", stderr +# callback null, false + +# checkPdfInfo = (pdfPath, callback = (error, output) ->) -> +# proc = ChildProcess.exec "pdfinfo #{fixturePath(pdfPath)}" +# stdout = "" +# proc.stdout.on "data", (chunk) -> stdout += chunk +# proc.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() +# proc.on "exit", () -> +# if stdout.match(/Optimized:\s+yes/) +# callback null, true +# else +# console.log "pdfinfo result", stdout +# callback null, false + +# compareMultiplePages = (project_id, callback = (error) ->) -> +# compareNext = (page_no, callback) -> +# path = "tmp/#{project_id}-source-#{page_no}.png" +# fs.stat fixturePath(path), (error, stat) -> +# if error? +# callback() +# else +# compare "tmp/#{project_id}-source-#{page_no}.png", "tmp/#{project_id}-generated-#{page_no}.png", (error, same) => +# throw error if error? +# same.should.equal true +# compareNext page_no + 1, callback +# compareNext 0, callback + +# comparePdf = (project_id, example_dir, callback = (error) ->) -> +# convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => +# throw error if error? +# convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => +# throw error if error? +# fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) => +# if error? +# compare "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) => +# throw error if error? +# same.should.equal true +# callback() +# else +# compareMultiplePages project_id, (error) -> +# throw error if error? +# callback() + +# downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) -> +# writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf")) +# request.get(url).pipe(writeStream) +# writeStream.on "close", () => +# checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) => +# throw error if error? +# optimised.should.equal true +# comparePdf project_id, example_dir, callback + +# Client.runServer(4242, fixturePath("examples")) + +# describe "Example Documents", -> +# before (done) -> +# ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> +# ClsiApp.ensureRunning done + + +# for example_dir in fs.readdirSync fixturePath("examples") +# do (example_dir) -> +# describe example_dir, -> +# before -> +# @project_id = Client.randomId() + "_" + example_dir + +# it "should generate the correct pdf", (done) -> +# Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => +# if error || body?.compile?.status is "failure" +# console.log "DEBUG: error", error, "body", JSON.stringify(body) +# pdf = Client.getOutputFile body, "pdf" +# downloadAndComparePdf(@project_id, example_dir, pdf.url, done) + +# it "should generate the correct pdf on the second run as well", (done) -> +# Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => +# if error || body?.compile?.status is "failure" +# console.log "DEBUG: error", error, "body", JSON.stringify(body) +# pdf = Client.getOutputFile body, "pdf" +# downloadAndComparePdf(@project_id, example_dir, pdf.url, done) diff --git a/test/acceptance/coffee/SimpleLatexFileTests.coffee b/test/acceptance/coffee/SimpleLatexFileTests.coffee index 95b667ba..ae21c61f 100644 --- a/test/acceptance/coffee/SimpleLatexFileTests.coffee +++ b/test/acceptance/coffee/SimpleLatexFileTests.coffee @@ -1,41 +1,41 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +# Client = require "./helpers/Client" +# request = require "request" +# require("chai").should() +# ClsiApp = require "./helpers/ClsiApp" -describe "Simple LaTeX file", -> - before (done) -> - @project_id = Client.randomId() - @request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' - ] - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() +# describe "Simple LaTeX file", -> +# before (done) -> +# @project_id = Client.randomId() +# @request = +# resources: [ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\begin{document} +# Hello world +# \\end{document} +# ''' +# ] +# ClsiApp.ensureRunning => +# Client.compile @project_id, @request, (@error, @res, @body) => done() - it "should return the PDF", -> - pdf = Client.getOutputFile(@body, "pdf") - pdf.type.should.equal "pdf" +# it "should return the PDF", -> +# pdf = Client.getOutputFile(@body, "pdf") +# pdf.type.should.equal "pdf" - it "should return the log", -> - log = Client.getOutputFile(@body, "log") - log.type.should.equal "log" +# it "should return the log", -> +# log = Client.getOutputFile(@body, "log") +# log.type.should.equal "log" - it "should provide the pdf for download", (done) -> - pdf = Client.getOutputFile(@body, "pdf") - request.get pdf.url, (error, res, body) -> - res.statusCode.should.equal 200 - done() +# it "should provide the pdf for download", (done) -> +# pdf = Client.getOutputFile(@body, "pdf") +# request.get pdf.url, (error, res, body) -> +# res.statusCode.should.equal 200 +# done() - it "should provide the log for download", (done) -> - log = Client.getOutputFile(@body, "pdf") - request.get log.url, (error, res, body) -> - res.statusCode.should.equal 200 - done() +# it "should provide the log for download", (done) -> +# log = Client.getOutputFile(@body, "pdf") +# request.get log.url, (error, res, body) -> +# res.statusCode.should.equal 200 +# done() diff --git a/test/acceptance/coffee/SynctexTests.coffee b/test/acceptance/coffee/SynctexTests.coffee index c6adcf2b..cbd22a79 100644 --- a/test/acceptance/coffee/SynctexTests.coffee +++ b/test/acceptance/coffee/SynctexTests.coffee @@ -20,14 +20,14 @@ describe "Syncing", -> ClsiApp.ensureRunning => Client.compile @project_id, @request, (@error, @res, @body) => done() - describe "from code to pdf", -> - it "should return the correct location", (done) -> - Client.syncFromCode @project_id, "main.tex", 3, 5, (error, pdfPositions) -> - throw error if error? - expect(pdfPositions).to.deep.equal( - pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] - ) - done() + # describe "from code to pdf", -> + # it "should return the correct location", (done) -> + # Client.syncFromCode @project_id, "main.tex", 3, 5, (error, pdfPositions) -> + # throw error if error? + # expect(pdfPositions).to.deep.equal( + # pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] + # ) + # done() describe "from pdf to code", -> it "should return the correct location", (done) -> diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index 38614871..2a0f6938 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -1,31 +1,31 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +# Client = require "./helpers/Client" +# request = require "request" +# require("chai").should() +# ClsiApp = require "./helpers/ClsiApp" -describe "Timed out compile", -> - before (done) -> - @request = - options: - timeout: 1 #seconds - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - Hello world - \\input{|"sleep 10"} - \\end{document} - ''' - ] - @project_id = Client.randomId() - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() +# describe "Timed out compile", -> +# before (done) -> +# @request = +# options: +# timeout: 1 #seconds +# resources: [ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\begin{document} +# Hello world +# \\input{|"sleep 10"} +# \\end{document} +# ''' +# ] +# @project_id = Client.randomId() +# ClsiApp.ensureRunning => +# Client.compile @project_id, @request, (@error, @res, @body) => done() - it "should return a timeout error", -> - @body.compile.error.should.equal "container timed out" +# it "should return a timeout error", -> +# @body.compile.error.should.equal "container timed out" - it "should return a timedout status", -> - @body.compile.status.should.equal "timedout" +# it "should return a timedout status", -> +# @body.compile.status.should.equal "timedout" diff --git a/test/acceptance/coffee/UrlCachingTests.coffee b/test/acceptance/coffee/UrlCachingTests.coffee index cef74467..89f67c28 100644 --- a/test/acceptance/coffee/UrlCachingTests.coffee +++ b/test/acceptance/coffee/UrlCachingTests.coffee @@ -1,222 +1,222 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -sinon = require "sinon" -ClsiApp = require "./helpers/ClsiApp" - -host = "localhost" - -Server = - run: () -> - express = require "express" - app = express() - - staticServer = express.static __dirname + "/../fixtures/" - app.get "/:random_id/*", (req, res, next) => - @getFile(req.url) - req.url = "/" + req.params[0] - staticServer(req, res, next) - - app.listen 31415, host - - getFile: () -> - - randomId: () -> - Math.random().toString(16).slice(2) - -Server.run() - -describe "Url Caching", -> - describe "Downloading an image for the first time", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = - resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - }] - - sinon.spy Server, "getFile" - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() - - afterEach -> - Server.getFile.restore() - - it "should download the image", -> - Server.getFile - .calledWith("/" + @file) - .should.equal true +# Client = require "./helpers/Client" +# request = require "request" +# require("chai").should() +# sinon = require "sinon" +# ClsiApp = require "./helpers/ClsiApp" + +# host = "localhost" + +# Server = +# run: () -> +# express = require "express" +# app = express() + +# staticServer = express.static __dirname + "/../fixtures/" +# app.get "/:random_id/*", (req, res, next) => +# @getFile(req.url) +# req.url = "/" + req.params[0] +# staticServer(req, res, next) + +# app.listen 31415, host + +# getFile: () -> + +# randomId: () -> +# Math.random().toString(16).slice(2) + +# Server.run() + +# describe "Url Caching", -> +# describe "Downloading an image for the first time", -> +# before (done) -> +# @project_id = Client.randomId() +# @file = "#{Server.randomId()}/lion.png" +# @request = +# resources: [{ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\usepackage{graphicx} +# \\begin{document} +# \\includegraphics{lion.png} +# \\end{document} +# ''' +# }, { +# path: "lion.png" +# url: "http://#{host}:31415/#{@file}" +# }] + +# sinon.spy Server, "getFile" +# ClsiApp.ensureRunning => +# Client.compile @project_id, @request, (@error, @res, @body) => done() + +# afterEach -> +# Server.getFile.restore() + +# it "should download the image", -> +# Server.getFile +# .calledWith("/" + @file) +# .should.equal true - describe "When an image is in the cache and the last modified date is unchanged", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = - resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: Date.now() - }] - - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - Client.compile @project_id, @request, (@error, @res, @body) => - done() - - after -> - Server.getFile.restore() - - it "should not download the image again", -> - Server.getFile.called.should.equal false - - describe "When an image is in the cache and the last modified date is advanced", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = - resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] - - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - @image_resource.modified = new Date(@last_modified + 3000) - Client.compile @project_id, @request, (@error, @res, @body) => - done() - - afterEach -> - Server.getFile.restore() - - it "should download the image again", -> - Server.getFile.called.should.equal true - - describe "When an image is in the cache and the last modified date is further in the past", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = - resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] - - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - @image_resource.modified = new Date(@last_modified - 3000) - Client.compile @project_id, @request, (@error, @res, @body) => - done() - - afterEach -> - Server.getFile.restore() - - it "should not download the image again", -> - Server.getFile.called.should.equal false - - describe "When an image is in the cache and the last modified date is not specified", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = - resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] - - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - delete @image_resource.modified - Client.compile @project_id, @request, (@error, @res, @body) => - done() - - afterEach -> - Server.getFile.restore() - - it "should download the image again", -> - Server.getFile.called.should.equal true +# describe "When an image is in the cache and the last modified date is unchanged", -> +# before (done) -> +# @project_id = Client.randomId() +# @file = "#{Server.randomId()}/lion.png" +# @request = +# resources: [{ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\usepackage{graphicx} +# \\begin{document} +# \\includegraphics{lion.png} +# \\end{document} +# ''' +# }, @image_resource = { +# path: "lion.png" +# url: "http://#{host}:31415/#{@file}" +# modified: Date.now() +# }] + +# Client.compile @project_id, @request, (@error, @res, @body) => +# sinon.spy Server, "getFile" +# Client.compile @project_id, @request, (@error, @res, @body) => +# done() + +# after -> +# Server.getFile.restore() + +# it "should not download the image again", -> +# Server.getFile.called.should.equal false + +# describe "When an image is in the cache and the last modified date is advanced", -> +# before (done) -> +# @project_id = Client.randomId() +# @file = "#{Server.randomId()}/lion.png" +# @request = +# resources: [{ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\usepackage{graphicx} +# \\begin{document} +# \\includegraphics{lion.png} +# \\end{document} +# ''' +# }, @image_resource = { +# path: "lion.png" +# url: "http://#{host}:31415/#{@file}" +# modified: @last_modified = Date.now() +# }] + +# Client.compile @project_id, @request, (@error, @res, @body) => +# sinon.spy Server, "getFile" +# @image_resource.modified = new Date(@last_modified + 3000) +# Client.compile @project_id, @request, (@error, @res, @body) => +# done() + +# afterEach -> +# Server.getFile.restore() + +# it "should download the image again", -> +# Server.getFile.called.should.equal true + +# describe "When an image is in the cache and the last modified date is further in the past", -> +# before (done) -> +# @project_id = Client.randomId() +# @file = "#{Server.randomId()}/lion.png" +# @request = +# resources: [{ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\usepackage{graphicx} +# \\begin{document} +# \\includegraphics{lion.png} +# \\end{document} +# ''' +# }, @image_resource = { +# path: "lion.png" +# url: "http://#{host}:31415/#{@file}" +# modified: @last_modified = Date.now() +# }] + +# Client.compile @project_id, @request, (@error, @res, @body) => +# sinon.spy Server, "getFile" +# @image_resource.modified = new Date(@last_modified - 3000) +# Client.compile @project_id, @request, (@error, @res, @body) => +# done() + +# afterEach -> +# Server.getFile.restore() + +# it "should not download the image again", -> +# Server.getFile.called.should.equal false + +# describe "When an image is in the cache and the last modified date is not specified", -> +# before (done) -> +# @project_id = Client.randomId() +# @file = "#{Server.randomId()}/lion.png" +# @request = +# resources: [{ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\usepackage{graphicx} +# \\begin{document} +# \\includegraphics{lion.png} +# \\end{document} +# ''' +# }, @image_resource = { +# path: "lion.png" +# url: "http://#{host}:31415/#{@file}" +# modified: @last_modified = Date.now() +# }] + +# Client.compile @project_id, @request, (@error, @res, @body) => +# sinon.spy Server, "getFile" +# delete @image_resource.modified +# Client.compile @project_id, @request, (@error, @res, @body) => +# done() + +# afterEach -> +# Server.getFile.restore() + +# it "should download the image again", -> +# Server.getFile.called.should.equal true - describe "After clearing the cache", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = - resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] - - Client.compile @project_id, @request, (error) => - throw error if error? - Client.clearCache @project_id, (error, res, body) => - throw error if error? - sinon.spy Server, "getFile" - Client.compile @project_id, @request, (@error, @res, @body) => - done() - - afterEach -> - Server.getFile.restore() - - it "should download the image again", -> - Server.getFile.called.should.equal true +# describe "After clearing the cache", -> +# before (done) -> +# @project_id = Client.randomId() +# @file = "#{Server.randomId()}/lion.png" +# @request = +# resources: [{ +# path: "main.tex" +# content: ''' +# \\documentclass{article} +# \\usepackage{graphicx} +# \\begin{document} +# \\includegraphics{lion.png} +# \\end{document} +# ''' +# }, @image_resource = { +# path: "lion.png" +# url: "http://#{host}:31415/#{@file}" +# modified: @last_modified = Date.now() +# }] + +# Client.compile @project_id, @request, (error) => +# throw error if error? +# Client.clearCache @project_id, (error, res, body) => +# throw error if error? +# sinon.spy Server, "getFile" +# Client.compile @project_id, @request, (@error, @res, @body) => +# done() + +# afterEach -> +# Server.getFile.restore() + +# it "should download the image again", -> +# Server.getFile.called.should.equal true diff --git a/test/acceptance/coffee/WordcountTests.coffee b/test/acceptance/coffee/WordcountTests.coffee index abace066..c41ba9ac 100644 --- a/test/acceptance/coffee/WordcountTests.coffee +++ b/test/acceptance/coffee/WordcountTests.coffee @@ -1,38 +1,38 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -expect = require("chai").expect -path = require("path") -fs = require("fs") -ClsiApp = require "./helpers/ClsiApp" +# Client = require "./helpers/Client" +# request = require "request" +# require("chai").should() +# expect = require("chai").expect +# path = require("path") +# fs = require("fs") +# ClsiApp = require "./helpers/ClsiApp" -describe "Syncing", -> - before (done) -> - @request = - resources: [ - path: "main.tex" - content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") - ] - @project_id = Client.randomId() - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() +# describe "Syncing", -> +# before (done) -> +# @request = +# resources: [ +# path: "main.tex" +# content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") +# ] +# @project_id = Client.randomId() +# ClsiApp.ensureRunning => +# Client.compile @project_id, @request, (@error, @res, @body) => done() - describe "wordcount file", -> - it "should return wordcount info", (done) -> - Client.wordcount @project_id, "main.tex", (error, result) -> - throw error if error? - expect(result).to.deep.equal( - texcount: { - encode: "utf8" - textWords: 2281 - headWords: 2 - outside: 0 - headers: 2 - elements: 0 - mathInline: 6 - mathDisplay: 0 - errors: 0 - messages: "" - } - ) - done() +# describe "wordcount file", -> +# it "should return wordcount info", (done) -> +# Client.wordcount @project_id, "main.tex", (error, result) -> +# throw error if error? +# expect(result).to.deep.equal( +# texcount: { +# encode: "utf8" +# textWords: 2281 +# headWords: 2 +# outside: 0 +# headers: 2 +# elements: 0 +# mathInline: 6 +# mathDisplay: 0 +# errors: 0 +# messages: "" +# } +# ) +# done() From 70f016af1f48c9f3b4fca8262e9c8d758cdc7918 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 1 Mar 2018 13:55:55 +0000 Subject: [PATCH 288/709] unit tests pass, acceptence fail uncomment tests --- Dockerfile | 2 +- app/coffee/CommandRunner.coffee | 2 - app/coffee/CompileManager.coffee | 17 +- app/coffee/LatexRunner.coffee | 1 - app/coffee/ResourceWriter.coffee | 1 - config/settings.defaults.coffee | 5 + docker-compose-config.yml | 3 +- docker-compose.yml | 21 +- docker-runner | 2 +- install_deps.sh | 2 +- .../coffee/BrokenLatexFileTests.coffee | 82 +++---- .../coffee/DeleteOldFilesTest.coffee | 60 ++--- .../coffee/ExampleDocumentTests.coffee | 223 +++++++++--------- .../coffee/SimpleLatexFileTests.coffee | 71 +++--- test/acceptance/coffee/SynctexTests.coffee | 28 ++- test/acceptance/coffee/TimeoutTests.coffee | 1 - test/acceptance/coffee/helpers/ClsiApp.coffee | 2 +- .../unit/coffee/CompileControllerTests.coffee | 2 +- test/unit/coffee/CompileManagerTests.coffee | 56 +++-- 19 files changed, 304 insertions(+), 277 deletions(-) diff --git a/Dockerfile b/Dockerfile index 881852da..f3172014 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,5 +23,5 @@ RUN useradd --user-group --create-home --home-dir /app --shell /bin/bash app RUN [ -e ./install_deps.sh ] && ./install_deps.sh -USER app +# USER app CMD ["node","app.js"] diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index 969b55f3..f47af001 100644 --- a/app/coffee/CommandRunner.coffee +++ b/app/coffee/CommandRunner.coffee @@ -5,9 +5,7 @@ logger.info "using standard command runner" module.exports = CommandRunner = run: (project_id, command, directory, image, timeout, environment, callback = (error) ->) -> - console.log("Command runner", directory) command = (arg.replace('$COMPILE_DIR', directory) for arg in command) - console.log("Command runner 2", command) logger.log project_id: project_id, command: command, directory: directory, "running command" logger.warn "timeouts and sandboxing are not enabled with CommandRunner" diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index a6df6dd5..0db15e52 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -41,8 +41,6 @@ module.exports = CompileManager = doCompile: (request, callback = (error, outputFiles) ->) -> compileDir = getCompileDir(request.project_id, request.user_id) - # console.log("doCompile", compileDir) - timer = new Metrics.Timer("write-to-disk") logger.log project_id: request.project_id, user_id: request.user_id, "syncing resources to disk" ResourceWriter.syncResourcesToDisk request, compileDir, (error, resourceList) -> @@ -206,7 +204,7 @@ module.exports = CompileManager = base_dir = Settings.path.synctexBaseDir(compileName) file_path = base_dir + "/" + file_name compileDir = getCompileDir(project_id, user_id) - synctex_path = "$COMPILE_DIR/output.pdf" + synctex_path = "#{base_dir}/output.pdf" command = ["code", synctex_path, file_path, line, column] CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> return callback(error) if error? @@ -219,9 +217,9 @@ module.exports = CompileManager = syncFromPdf: (project_id, user_id, page, h, v, callback = (error, filePositions) ->) -> compileName = getCompileName(project_id, user_id) - base_dir = Settings.path.synctexBaseDir(compileName) compileDir = getCompileDir(project_id, user_id) - synctex_path = "$COMPILE_DIR/output.pdf" + base_dir = Settings.path.synctexBaseDir(compileName) + synctex_path = "#{base_dir}/output.pdf" command = ["pdf", synctex_path, page, h, v] CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> return callback(error) if error? @@ -245,19 +243,16 @@ module.exports = CompileManager = _runSynctex: (project_id, user_id, command, callback = (error, stdout) ->) -> seconds = 1000 - #this is a hack, only works for docker runner command.unshift("/opt/synctex") + directory = getCompileDir(project_id, user_id) timeout = 10 * 1000 compileName = getCompileName(project_id, user_id) - console.log command, "_runSynctex" - - CommandRunner.run compileName, command, directory, Settings.clsi.docker.image, timeout, {}, (error, stdout) -> - console.log("synctex run", stdout) + CommandRunner.run compileName, command, directory, Settings.clsi.docker.image, timeout, {}, (error, output) -> if error? logger.err err:error, command:command, "error running synctex" return callback(error) - callback(null, stdout) + callback(null, output.stdout) _parseSynctexFromCodeOutput: (output) -> results = [] diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 11e71e53..6a5a4f6a 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -8,7 +8,6 @@ ProcessTable = {} # table of currently running jobs (pids or docker container n module.exports = LatexRunner = runLatex: (project_id, options, callback = (error) ->) -> - console.log("LatexRunner", options.directory) {directory, mainFile, compiler, timeout, image, environment} = options compiler ||= "pdflatex" timeout ||= 60000 # milliseconds diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 66cfbfa0..0b6aef5b 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -109,7 +109,6 @@ module.exports = ResourceWriter = callback() _writeResourceToDisk: (project_id, resource, basePath, callback = (error) ->) -> - console.log("_writeResourceToDisk", basePath, resource.path) ResourceWriter.checkPath basePath, resource.path, (error, path) -> return callback(error) if error? mkdirp Path.dirname(path), (error) -> diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index d09784de..2f2348b0 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -40,4 +40,9 @@ if process.env["COMMAND_RUNNER"] expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 checkProjectsIntervalMs: 10 * 60 * 1000 + module.exports.path.synctexBaseDir = -> "/compile" + module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] + + #TODO this can be deleted once module is merged in + module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"] diff --git a/docker-compose-config.yml b/docker-compose-config.yml index 3d64975f..16897c4d 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -8,6 +8,7 @@ services: SHARELATEX_CONFIG: /app/config/settings.defaults.coffee COMMAND_RUNNER: docker-runner-sharelatex COMPILES_HOST_DIR: $PWD/compiles + SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex volumes: - /var/run/docker.sock:/var/run/docker.sock - ./docker-runner:/app/node_modules/docker-runner-sharelatex @@ -21,6 +22,6 @@ services: COMMAND_RUNNER: docker-runner-sharelatex COMPILES_HOST_DIR: $PWD/compiles volumes: - - /var/run/docker.sock:/var/run/docker.sock + - /var/run/docker.sock:/var/run/docker.sock:rw - ./docker-runner:/app/node_modules/docker-runner-sharelatex - ./compiles:/app/compiles \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 090c3602..0ed0b544 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,12 +32,29 @@ services: synctex: image: quay.io/sharelatex/texlive-full:2017.1 volumes: - - ~/Projects/sharelatex-dev-environment/clsi/compiles/564c29f884179:/compile + - ~/Projects/sharelatex-dev-environment/clsi/compiles/cd749215b3512:/compile - ./bin/synctex:/opt/synctex - command: + entrypoint: /opt/synctex pdf /compile/output.pdf 1 100 200 + # /opt/synctex code -h + # /opt/synctex code /compile/main.tex ./main.tex 3 5 + # ls -al + app: + build: . + volumes: + - .:/app + working_dir: /app + extends: + file: docker-compose-config.yml + service: dev + environment: + REDIS_HOST: redis + MONGO_HOST: mongo + depends_on: + - redis + - mongo redis: diff --git a/docker-runner b/docker-runner index 9a732b25..65ad6211 160000 --- a/docker-runner +++ b/docker-runner @@ -1 +1 @@ -Subproject commit 9a732b2594f014a722f0c16150cf848b9512430f +Subproject commit 65ad62116fb1ba4fca978a6d1d6b89bf195e5166 diff --git a/install_deps.sh b/install_deps.sh index 3caa1c7c..bde142a5 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -1,6 +1,6 @@ /bin/sh wget -qO- https://get.docker.com/ | sh -apt-get install poppler-utils ghostscript --yes +apt-get install poppler-utils vim ghostscript --yes npm rebuild usermod -aG docker app diff --git a/test/acceptance/coffee/BrokenLatexFileTests.coffee b/test/acceptance/coffee/BrokenLatexFileTests.coffee index 755f28e7..8ab4344f 100644 --- a/test/acceptance/coffee/BrokenLatexFileTests.coffee +++ b/test/acceptance/coffee/BrokenLatexFileTests.coffee @@ -1,48 +1,48 @@ -# Client = require "./helpers/Client" -# request = require "request" -# require("chai").should() -# ClsiApp = require "./helpers/ClsiApp" +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +ClsiApp = require "./helpers/ClsiApp" -# describe "Broken LaTeX file", -> -# before (done)-> -# @broken_request = -# resources: [ -# path: "main.tex" -# content: ''' -# \\documentclass{articl % :( -# \\begin{documen % :( -# Broken -# \\end{documen % :( -# ''' -# ] -# @correct_request = -# resources: [ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\begin{document} -# Hello world -# \\end{document} -# ''' -# ] -# ClsiApp.ensureRunning done +describe "Broken LaTeX file", -> + before (done)-> + @broken_request = + resources: [ + path: "main.tex" + content: ''' + \\documentclass{articl % :( + \\begin{documen % :( + Broken + \\end{documen % :( + ''' + ] + @correct_request = + resources: [ + path: "main.tex" + content: ''' + \\documentclass{article} + \\begin{document} + Hello world + \\end{document} + ''' + ] + ClsiApp.ensureRunning done -# describe "on first run", -> -# before (done) -> -# @project_id = Client.randomId() -# Client.compile @project_id, @broken_request, (@error, @res, @body) => done() + describe "on first run", -> + before (done) -> + @project_id = Client.randomId() + Client.compile @project_id, @broken_request, (@error, @res, @body) => done() -# it "should return a failure status", -> -# @body.compile.status.should.equal "failure" + it "should return a failure status", -> + @body.compile.status.should.equal "failure" -# describe "on second run", -> -# before (done) -> -# @project_id = Client.randomId() -# Client.compile @project_id, @correct_request, () => -# Client.compile @project_id, @broken_request, (@error, @res, @body) => -# done() + describe "on second run", -> + before (done) -> + @project_id = Client.randomId() + Client.compile @project_id, @correct_request, () => + Client.compile @project_id, @broken_request, (@error, @res, @body) => + done() -# it "should return a failure status", -> -# @body.compile.status.should.equal "failure" + it "should return a failure status", -> + @body.compile.status.should.equal "failure" diff --git a/test/acceptance/coffee/DeleteOldFilesTest.coffee b/test/acceptance/coffee/DeleteOldFilesTest.coffee index 750f5f93..1cb67765 100644 --- a/test/acceptance/coffee/DeleteOldFilesTest.coffee +++ b/test/acceptance/coffee/DeleteOldFilesTest.coffee @@ -1,36 +1,36 @@ -# Client = require "./helpers/Client" -# request = require "request" -# require("chai").should() -# ClsiApp = require "./helpers/ClsiApp" +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +ClsiApp = require "./helpers/ClsiApp" -# describe "Deleting Old Files", -> -# before (done)-> -# @request = -# resources: [ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\begin{document} -# Hello world -# \\end{document} -# ''' -# ] -# ClsiApp.ensureRunning done +describe "Deleting Old Files", -> + before (done)-> + @request = + resources: [ + path: "main.tex" + content: ''' + \\documentclass{article} + \\begin{document} + Hello world + \\end{document} + ''' + ] + ClsiApp.ensureRunning done -# describe "on first run", -> -# before (done) -> -# @project_id = Client.randomId() -# Client.compile @project_id, @request, (@error, @res, @body) => done() + describe "on first run", -> + before (done) -> + @project_id = Client.randomId() + Client.compile @project_id, @request, (@error, @res, @body) => done() -# it "should return a success status", -> -# @body.compile.status.should.equal "success" + it "should return a success status", -> + @body.compile.status.should.equal "success" -# describe "after file has been deleted", -> -# before (done) -> -# @request.resources = [] -# Client.compile @project_id, @request, (@error, @res, @body) => -# done() + describe "after file has been deleted", -> + before (done) -> + @request.resources = [] + Client.compile @project_id, @request, (@error, @res, @body) => + done() -# it "should return a failure status", -> -# @body.compile.status.should.equal "failure" + it "should return a failure status", -> + @body.compile.status.should.equal "failure" diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 1a1a0a0a..ec569796 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -1,114 +1,113 @@ -# Client = require "./helpers/Client" -# request = require "request" -# require("chai").should() -# fs = require "fs" -# ChildProcess = require "child_process" -# ClsiApp = require "./helpers/ClsiApp" - -# fixturePath = (path) -> __dirname + "/../fixtures/" + path - -# try -# fs.mkdirSync(fixturePath("tmp")) -# catch e - -# convertToPng = (pdfPath, pngPath, callback = (error) ->) -> -# command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" -# convert = ChildProcess.exec command -# stdout = "" -# convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() -# convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() -# convert.on "exit", () -> -# callback() - -# compare = (originalPath, generatedPath, callback = (error, same) ->) -> -# diff_file = "#{fixturePath(generatedPath)}-diff.png" -# proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{diff_file}" -# stderr = "" -# proc.stderr.on "data", (chunk) -> stderr += chunk -# proc.on "exit", () -> -# if stderr.trim() == "0 (0)" -# fs.unlink diff_file # remove output diff if test matches expected image -# callback null, true -# else -# console.log "compare result", stderr -# callback null, false - -# checkPdfInfo = (pdfPath, callback = (error, output) ->) -> -# proc = ChildProcess.exec "pdfinfo #{fixturePath(pdfPath)}" -# stdout = "" -# proc.stdout.on "data", (chunk) -> stdout += chunk -# proc.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() -# proc.on "exit", () -> -# if stdout.match(/Optimized:\s+yes/) -# callback null, true -# else -# console.log "pdfinfo result", stdout -# callback null, false - -# compareMultiplePages = (project_id, callback = (error) ->) -> -# compareNext = (page_no, callback) -> -# path = "tmp/#{project_id}-source-#{page_no}.png" -# fs.stat fixturePath(path), (error, stat) -> -# if error? -# callback() -# else -# compare "tmp/#{project_id}-source-#{page_no}.png", "tmp/#{project_id}-generated-#{page_no}.png", (error, same) => -# throw error if error? -# same.should.equal true -# compareNext page_no + 1, callback -# compareNext 0, callback - -# comparePdf = (project_id, example_dir, callback = (error) ->) -> -# convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => -# throw error if error? -# convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => -# throw error if error? -# fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) => -# if error? -# compare "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) => -# throw error if error? -# same.should.equal true -# callback() -# else -# compareMultiplePages project_id, (error) -> -# throw error if error? -# callback() - -# downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) -> -# writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf")) -# request.get(url).pipe(writeStream) -# writeStream.on "close", () => -# checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) => -# throw error if error? -# optimised.should.equal true -# comparePdf project_id, example_dir, callback - -# Client.runServer(4242, fixturePath("examples")) - -# describe "Example Documents", -> -# before (done) -> -# ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> -# ClsiApp.ensureRunning done - - -# for example_dir in fs.readdirSync fixturePath("examples") -# do (example_dir) -> -# describe example_dir, -> -# before -> -# @project_id = Client.randomId() + "_" + example_dir - -# it "should generate the correct pdf", (done) -> -# Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => -# if error || body?.compile?.status is "failure" -# console.log "DEBUG: error", error, "body", JSON.stringify(body) -# pdf = Client.getOutputFile body, "pdf" -# downloadAndComparePdf(@project_id, example_dir, pdf.url, done) - -# it "should generate the correct pdf on the second run as well", (done) -> -# Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => -# if error || body?.compile?.status is "failure" -# console.log "DEBUG: error", error, "body", JSON.stringify(body) -# pdf = Client.getOutputFile body, "pdf" -# downloadAndComparePdf(@project_id, example_dir, pdf.url, done) +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +fs = require "fs" +ChildProcess = require "child_process" +ClsiApp = require "./helpers/ClsiApp" + +fixturePath = (path) -> __dirname + "/../fixtures/" + path + +try + fs.mkdirSync(fixturePath("tmp")) +catch e + +convertToPng = (pdfPath, pngPath, callback = (error) ->) -> + command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" + convert = ChildProcess.exec command + stdout = "" + convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() + convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() + convert.on "exit", () -> + callback() + +compare = (originalPath, generatedPath, callback = (error, same) ->) -> + diff_file = "#{fixturePath(generatedPath)}-diff.png" + proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{diff_file}" + stderr = "" + proc.stderr.on "data", (chunk) -> stderr += chunk + proc.on "exit", () -> + if stderr.trim() == "0 (0)" + fs.unlink diff_file # remove output diff if test matches expected image + callback null, true + else + console.log "compare result", stderr + callback null, false + +checkPdfInfo = (pdfPath, callback = (error, output) ->) -> + proc = ChildProcess.exec "pdfinfo #{fixturePath(pdfPath)}" + stdout = "" + proc.stdout.on "data", (chunk) -> stdout += chunk + proc.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() + proc.on "exit", () -> + if stdout.match(/Optimized:\s+yes/) + callback null, true + else + callback null, false + +compareMultiplePages = (project_id, callback = (error) ->) -> + compareNext = (page_no, callback) -> + path = "tmp/#{project_id}-source-#{page_no}.png" + fs.stat fixturePath(path), (error, stat) -> + if error? + callback() + else + compare "tmp/#{project_id}-source-#{page_no}.png", "tmp/#{project_id}-generated-#{page_no}.png", (error, same) => + throw error if error? + same.should.equal true + compareNext page_no + 1, callback + compareNext 0, callback + +comparePdf = (project_id, example_dir, callback = (error) ->) -> + convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => + throw error if error? + convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => + throw error if error? + fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) => + if error? + compare "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) => + throw error if error? + same.should.equal true + callback() + else + compareMultiplePages project_id, (error) -> + throw error if error? + callback() + +downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) -> + writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf")) + request.get(url).pipe(writeStream) + writeStream.on "close", () => + checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) => + throw error if error? + optimised.should.equal true + comparePdf project_id, example_dir, callback + +Client.runServer(4242, fixturePath("examples")) + +describe "Example Documents", -> + before (done) -> + ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> + ClsiApp.ensureRunning done + + + for example_dir in fs.readdirSync fixturePath("examples") + do (example_dir) -> + describe example_dir, -> + before -> + @project_id = Client.randomId() + "_" + example_dir + + it "should generate the correct pdf", (done) -> + Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => + if error || body?.compile?.status is "failure" + console.log "DEBUG: error", error, "body", JSON.stringify(body) + pdf = Client.getOutputFile body, "pdf" + downloadAndComparePdf(@project_id, example_dir, pdf.url, done) + + it "should generate the correct pdf on the second run as well", (done) -> + Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => + if error || body?.compile?.status is "failure" + console.log "DEBUG: error", error, "body", JSON.stringify(body) + pdf = Client.getOutputFile body, "pdf" + downloadAndComparePdf(@project_id, example_dir, pdf.url, done) diff --git a/test/acceptance/coffee/SimpleLatexFileTests.coffee b/test/acceptance/coffee/SimpleLatexFileTests.coffee index ae21c61f..0d3337ad 100644 --- a/test/acceptance/coffee/SimpleLatexFileTests.coffee +++ b/test/acceptance/coffee/SimpleLatexFileTests.coffee @@ -1,41 +1,42 @@ -# Client = require "./helpers/Client" -# request = require "request" -# require("chai").should() -# ClsiApp = require "./helpers/ClsiApp" +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +ClsiApp = require "./helpers/ClsiApp" -# describe "Simple LaTeX file", -> -# before (done) -> -# @project_id = Client.randomId() -# @request = -# resources: [ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\begin{document} -# Hello world -# \\end{document} -# ''' -# ] -# ClsiApp.ensureRunning => -# Client.compile @project_id, @request, (@error, @res, @body) => done() +describe "Simple LaTeX file", -> + before (done) -> + @project_id = Client.randomId() + @request = + resources: [ + path: "main.tex" + content: ''' + \\documentclass{article} + \\begin{document} + Hello world + \\end{document} + ''' + ] + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() -# it "should return the PDF", -> -# pdf = Client.getOutputFile(@body, "pdf") -# pdf.type.should.equal "pdf" + it "should return the PDF", -> + pdf = Client.getOutputFile(@body, "pdf") + console.log @body + pdf.type.should.equal "pdf" -# it "should return the log", -> -# log = Client.getOutputFile(@body, "log") -# log.type.should.equal "log" + it "should return the log", -> + log = Client.getOutputFile(@body, "log") + log.type.should.equal "log" -# it "should provide the pdf for download", (done) -> -# pdf = Client.getOutputFile(@body, "pdf") -# request.get pdf.url, (error, res, body) -> -# res.statusCode.should.equal 200 -# done() + it "should provide the pdf for download", (done) -> + pdf = Client.getOutputFile(@body, "pdf") + request.get pdf.url, (error, res, body) -> + res.statusCode.should.equal 200 + done() -# it "should provide the log for download", (done) -> -# log = Client.getOutputFile(@body, "pdf") -# request.get log.url, (error, res, body) -> -# res.statusCode.should.equal 200 -# done() + it "should provide the log for download", (done) -> + log = Client.getOutputFile(@body, "pdf") + request.get log.url, (error, res, body) -> + res.statusCode.should.equal 200 + done() diff --git a/test/acceptance/coffee/SynctexTests.coffee b/test/acceptance/coffee/SynctexTests.coffee index cbd22a79..685d2928 100644 --- a/test/acceptance/coffee/SynctexTests.coffee +++ b/test/acceptance/coffee/SynctexTests.coffee @@ -3,35 +3,37 @@ request = require "request" require("chai").should() expect = require("chai").expect ClsiApp = require "./helpers/ClsiApp" +crypto = require("crypto") describe "Syncing", -> before (done) -> - @request = - resources: [ - path: "main.tex" - content: ''' + content = ''' \\documentclass{article} \\begin{document} Hello world \\end{document} ''' + @request = + resources: [ + path: "main.tex" + content: content ] @project_id = Client.randomId() ClsiApp.ensureRunning => Client.compile @project_id, @request, (@error, @res, @body) => done() - # describe "from code to pdf", -> - # it "should return the correct location", (done) -> - # Client.syncFromCode @project_id, "main.tex", 3, 5, (error, pdfPositions) -> - # throw error if error? - # expect(pdfPositions).to.deep.equal( - # pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] - # ) - # done() + describe "from code to pdf", -> + it "should return the correct location", (done) -> + Client.syncFromCode @project_id, "main.tex", 3, 5, (error, pdfPositions) -> + throw error if error? + expect(pdfPositions).to.deep.equal( + pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] + ) + done() describe "from pdf to code", -> it "should return the correct location", (done) -> - Client.syncFromPdf @project_id, 1, 100, 200, (error, codePositions) -> + Client.syncFromPdf @project_id, 1, 100, 200, (error, codePositions) => throw error if error? expect(codePositions).to.deep.equal( code: [ { file: 'main.tex', line: 3, column: -1 } ] diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index 2a0f6938..bc9a1423 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -15,7 +15,6 @@ # \\documentclass{article} # \\begin{document} # Hello world -# \\input{|"sleep 10"} # \\end{document} # ''' # ] diff --git a/test/acceptance/coffee/helpers/ClsiApp.coffee b/test/acceptance/coffee/helpers/ClsiApp.coffee index 35be427b..d9cd534b 100644 --- a/test/acceptance/coffee/helpers/ClsiApp.coffee +++ b/test/acceptance/coffee/helpers/ClsiApp.coffee @@ -1,5 +1,5 @@ app = require('../../../../app') -require("logger-sharelatex").logger.level("error") +require("logger-sharelatex").logger.level("info") logger = require("logger-sharelatex") Settings = require("settings-sharelatex") diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index 7b6001d0..f0269ee3 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -14,7 +14,7 @@ describe "CompileController", -> clsi: url: "http://clsi.example.com" "./ProjectPersistenceManager": @ProjectPersistenceManager = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err:sinon.stub() } @Settings.externalUrl = "http://www.example.com" @req = {} @res = {} diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 341ce2d0..448e06c0 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -13,7 +13,14 @@ describe "CompileManager", -> "./ResourceWriter": @ResourceWriter = {} "./OutputFileFinder": @OutputFileFinder = {} "./OutputCacheManager": @OutputCacheManager = {} - "settings-sharelatex": @Settings = { path: compilesDir: "/compiles/dir" } + "settings-sharelatex": @Settings = + path: + compilesDir: "/compiles/dir" + synctexBaseDir: -> "/compile" + clsi: + docker: + image: "SOMEIMAGE" + "logger-sharelatex": @logger = { log: sinon.stub() , info:->} "child_process": @child_process = {} "./CommandRunner": @CommandRunner = {} @@ -23,13 +30,14 @@ describe "CompileManager", -> "fs": @fs = {} "fs-extra": @fse = { ensureDir: sinon.stub().callsArg(1) } @callback = sinon.stub() - + @project_id = "project-id-123" + @user_id = "1234" describe "doCompileWithLock", -> beforeEach -> @request = resources: @resources = "mock-resources" - project_id: @project_id = "project-id-123" - user_id: @user_id = "1234" + project_id: @project_id + user_id: @user_id @output_files = ["foo", "bar"] @Settings.compileDir = "compiles" @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @@ -95,8 +103,8 @@ describe "CompileManager", -> @request = resources: @resources = "mock-resources" rootResourcePath: @rootResourcePath = "main.tex" - project_id: @project_id = "project-id-123" - user_id: @user_id = "1234" + project_id: @project_id + user_id: @user_id compiler: @compiler = "pdflatex" timeout: @timeout = 42000 imageName: @image = "example.com/image" @@ -247,16 +255,17 @@ describe "CompileManager", -> describe "syncFromCode", -> beforeEach -> @fs.stat = sinon.stub().callsArgWith(1, null,{isFile: ()->true}) - @child_process.execFile.callsArgWith(3, null, @stdout = "NODE\t#{@page}\t#{@h}\t#{@v}\t#{@width}\t#{@height}\n", "") + @stdout = "NODE\t#{@page}\t#{@h}\t#{@v}\t#{@width}\t#{@height}\n" + @CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout}) @CompileManager.syncFromCode @project_id, @user_id, @file_name, @line, @column, @callback - it "should execute the synctex binary", -> - bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" - file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}" - @child_process.execFile - .calledWith(bin_path, ["code", synctex_path, file_path, @line, @column], timeout: 10000) - .should.equal true + # it "should execute the synctex binary", -> + # bin_path = Path.resolve(__dirname + "/../../../bin/synctex") + # synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" + # file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}" + # @child_process.execFile + # .calledWith(bin_path, ["code", synctex_path, file_path, @line, @column], timeout: 10000) + # .should.equal true it "should call the callback with the parsed output", -> @callback @@ -272,17 +281,20 @@ describe "CompileManager", -> describe "syncFromPdf", -> beforeEach -> @fs.stat = sinon.stub().callsArgWith(1, null,{isFile: ()->true}) - @child_process.execFile.callsArgWith(3, null, @stdout = "NODE\t#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}\t#{@line}\t#{@column}\n", "") + @stdout = "NODE\t#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}\t#{@line}\t#{@column}\n" + @CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout}) @CompileManager.syncFromPdf @project_id, @user_id, @page, @h, @v, @callback - it "should execute the synctex binary", -> - bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" - @child_process.execFile - .calledWith(bin_path, ["pdf", synctex_path, @page, @h, @v], timeout: 10000) - .should.equal true + # it "should execute the synctex binary", -> + # bin_path = Path.resolve(__dirname + "/../../../bin/synctex") + # synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" + # @CommandRunner.run + # .calledWith(bin_path, ["pdf", synctex_path, @page, @h, @v], timeout: 10000) + # .should.equal true it "should call the callback with the parsed output", -> + console.log(@file_name, @line, @column) + console.log @callback.args[0] @callback .calledWith(null, [{ file: @file_name @@ -297,7 +309,7 @@ describe "CompileManager", -> @fs.readFile = sinon.stub().callsArgWith(2, null, @stdout = "Encoding: ascii\nWords in text: 2") @callback = sinon.stub() - @project_id = "project-id-123" + @project_id @timeout = 10 * 1000 @file_name = "main.tex" @Settings.path.compilesDir = "/local/compile/directory" From 1a47887e80d935a72cb52f861a172ab39f140f66 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 2 Mar 2018 17:58:34 +0000 Subject: [PATCH 289/709] make timeout latex more complex(slower) --- test/acceptance/coffee/TimeoutTests.coffee | 51 +++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index bc9a1423..9195474a 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -1,30 +1,31 @@ -# Client = require "./helpers/Client" -# request = require "request" -# require("chai").should() -# ClsiApp = require "./helpers/ClsiApp" +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +ClsiApp = require "./helpers/ClsiApp" -# describe "Timed out compile", -> -# before (done) -> -# @request = -# options: -# timeout: 1 #seconds -# resources: [ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\begin{document} -# Hello world -# \\end{document} -# ''' -# ] -# @project_id = Client.randomId() -# ClsiApp.ensureRunning => -# Client.compile @project_id, @request, (@error, @res, @body) => done() +describe "Timed out compile", -> + before (done) -> + @request = + options: + timeout: 1 #seconds + resources: [ + path: "main.tex" + content: ''' + \\documentclass{article} + \\begin{document} + \\input{|"/bin/bash -c ':(){ :|:& };:'"} + \\end{document} + ''' + ] + @project_id = Client.randomId() + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() -# it "should return a timeout error", -> -# @body.compile.error.should.equal "container timed out" + it "should return a timeout error", -> + console.log @body.compile, "!!!1111" + @body.compile.error.should.equal "container timed out" -# it "should return a timedout status", -> -# @body.compile.status.should.equal "timedout" + it "should return a timedout status", -> + @body.compile.status.should.equal "timedout" From 5cb3bfcbbb15af20a2034bf3be1683d9513ea4ca Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 2 Mar 2018 17:59:37 +0000 Subject: [PATCH 290/709] uncomment tests --- test/acceptance/coffee/UrlCachingTests.coffee | 436 +++++++++--------- test/acceptance/coffee/WordcountTests.coffee | 72 +-- 2 files changed, 254 insertions(+), 254 deletions(-) diff --git a/test/acceptance/coffee/UrlCachingTests.coffee b/test/acceptance/coffee/UrlCachingTests.coffee index 89f67c28..cef74467 100644 --- a/test/acceptance/coffee/UrlCachingTests.coffee +++ b/test/acceptance/coffee/UrlCachingTests.coffee @@ -1,222 +1,222 @@ -# Client = require "./helpers/Client" -# request = require "request" -# require("chai").should() -# sinon = require "sinon" -# ClsiApp = require "./helpers/ClsiApp" - -# host = "localhost" - -# Server = -# run: () -> -# express = require "express" -# app = express() - -# staticServer = express.static __dirname + "/../fixtures/" -# app.get "/:random_id/*", (req, res, next) => -# @getFile(req.url) -# req.url = "/" + req.params[0] -# staticServer(req, res, next) - -# app.listen 31415, host - -# getFile: () -> - -# randomId: () -> -# Math.random().toString(16).slice(2) - -# Server.run() - -# describe "Url Caching", -> -# describe "Downloading an image for the first time", -> -# before (done) -> -# @project_id = Client.randomId() -# @file = "#{Server.randomId()}/lion.png" -# @request = -# resources: [{ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\usepackage{graphicx} -# \\begin{document} -# \\includegraphics{lion.png} -# \\end{document} -# ''' -# }, { -# path: "lion.png" -# url: "http://#{host}:31415/#{@file}" -# }] - -# sinon.spy Server, "getFile" -# ClsiApp.ensureRunning => -# Client.compile @project_id, @request, (@error, @res, @body) => done() - -# afterEach -> -# Server.getFile.restore() - -# it "should download the image", -> -# Server.getFile -# .calledWith("/" + @file) -# .should.equal true +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +sinon = require "sinon" +ClsiApp = require "./helpers/ClsiApp" + +host = "localhost" + +Server = + run: () -> + express = require "express" + app = express() + + staticServer = express.static __dirname + "/../fixtures/" + app.get "/:random_id/*", (req, res, next) => + @getFile(req.url) + req.url = "/" + req.params[0] + staticServer(req, res, next) + + app.listen 31415, host + + getFile: () -> + + randomId: () -> + Math.random().toString(16).slice(2) + +Server.run() + +describe "Url Caching", -> + describe "Downloading an image for the first time", -> + before (done) -> + @project_id = Client.randomId() + @file = "#{Server.randomId()}/lion.png" + @request = + resources: [{ + path: "main.tex" + content: ''' + \\documentclass{article} + \\usepackage{graphicx} + \\begin{document} + \\includegraphics{lion.png} + \\end{document} + ''' + }, { + path: "lion.png" + url: "http://#{host}:31415/#{@file}" + }] + + sinon.spy Server, "getFile" + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() + + afterEach -> + Server.getFile.restore() + + it "should download the image", -> + Server.getFile + .calledWith("/" + @file) + .should.equal true -# describe "When an image is in the cache and the last modified date is unchanged", -> -# before (done) -> -# @project_id = Client.randomId() -# @file = "#{Server.randomId()}/lion.png" -# @request = -# resources: [{ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\usepackage{graphicx} -# \\begin{document} -# \\includegraphics{lion.png} -# \\end{document} -# ''' -# }, @image_resource = { -# path: "lion.png" -# url: "http://#{host}:31415/#{@file}" -# modified: Date.now() -# }] - -# Client.compile @project_id, @request, (@error, @res, @body) => -# sinon.spy Server, "getFile" -# Client.compile @project_id, @request, (@error, @res, @body) => -# done() - -# after -> -# Server.getFile.restore() - -# it "should not download the image again", -> -# Server.getFile.called.should.equal false - -# describe "When an image is in the cache and the last modified date is advanced", -> -# before (done) -> -# @project_id = Client.randomId() -# @file = "#{Server.randomId()}/lion.png" -# @request = -# resources: [{ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\usepackage{graphicx} -# \\begin{document} -# \\includegraphics{lion.png} -# \\end{document} -# ''' -# }, @image_resource = { -# path: "lion.png" -# url: "http://#{host}:31415/#{@file}" -# modified: @last_modified = Date.now() -# }] - -# Client.compile @project_id, @request, (@error, @res, @body) => -# sinon.spy Server, "getFile" -# @image_resource.modified = new Date(@last_modified + 3000) -# Client.compile @project_id, @request, (@error, @res, @body) => -# done() - -# afterEach -> -# Server.getFile.restore() - -# it "should download the image again", -> -# Server.getFile.called.should.equal true - -# describe "When an image is in the cache and the last modified date is further in the past", -> -# before (done) -> -# @project_id = Client.randomId() -# @file = "#{Server.randomId()}/lion.png" -# @request = -# resources: [{ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\usepackage{graphicx} -# \\begin{document} -# \\includegraphics{lion.png} -# \\end{document} -# ''' -# }, @image_resource = { -# path: "lion.png" -# url: "http://#{host}:31415/#{@file}" -# modified: @last_modified = Date.now() -# }] - -# Client.compile @project_id, @request, (@error, @res, @body) => -# sinon.spy Server, "getFile" -# @image_resource.modified = new Date(@last_modified - 3000) -# Client.compile @project_id, @request, (@error, @res, @body) => -# done() - -# afterEach -> -# Server.getFile.restore() - -# it "should not download the image again", -> -# Server.getFile.called.should.equal false - -# describe "When an image is in the cache and the last modified date is not specified", -> -# before (done) -> -# @project_id = Client.randomId() -# @file = "#{Server.randomId()}/lion.png" -# @request = -# resources: [{ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\usepackage{graphicx} -# \\begin{document} -# \\includegraphics{lion.png} -# \\end{document} -# ''' -# }, @image_resource = { -# path: "lion.png" -# url: "http://#{host}:31415/#{@file}" -# modified: @last_modified = Date.now() -# }] - -# Client.compile @project_id, @request, (@error, @res, @body) => -# sinon.spy Server, "getFile" -# delete @image_resource.modified -# Client.compile @project_id, @request, (@error, @res, @body) => -# done() - -# afterEach -> -# Server.getFile.restore() - -# it "should download the image again", -> -# Server.getFile.called.should.equal true + describe "When an image is in the cache and the last modified date is unchanged", -> + before (done) -> + @project_id = Client.randomId() + @file = "#{Server.randomId()}/lion.png" + @request = + resources: [{ + path: "main.tex" + content: ''' + \\documentclass{article} + \\usepackage{graphicx} + \\begin{document} + \\includegraphics{lion.png} + \\end{document} + ''' + }, @image_resource = { + path: "lion.png" + url: "http://#{host}:31415/#{@file}" + modified: Date.now() + }] + + Client.compile @project_id, @request, (@error, @res, @body) => + sinon.spy Server, "getFile" + Client.compile @project_id, @request, (@error, @res, @body) => + done() + + after -> + Server.getFile.restore() + + it "should not download the image again", -> + Server.getFile.called.should.equal false + + describe "When an image is in the cache and the last modified date is advanced", -> + before (done) -> + @project_id = Client.randomId() + @file = "#{Server.randomId()}/lion.png" + @request = + resources: [{ + path: "main.tex" + content: ''' + \\documentclass{article} + \\usepackage{graphicx} + \\begin{document} + \\includegraphics{lion.png} + \\end{document} + ''' + }, @image_resource = { + path: "lion.png" + url: "http://#{host}:31415/#{@file}" + modified: @last_modified = Date.now() + }] + + Client.compile @project_id, @request, (@error, @res, @body) => + sinon.spy Server, "getFile" + @image_resource.modified = new Date(@last_modified + 3000) + Client.compile @project_id, @request, (@error, @res, @body) => + done() + + afterEach -> + Server.getFile.restore() + + it "should download the image again", -> + Server.getFile.called.should.equal true + + describe "When an image is in the cache and the last modified date is further in the past", -> + before (done) -> + @project_id = Client.randomId() + @file = "#{Server.randomId()}/lion.png" + @request = + resources: [{ + path: "main.tex" + content: ''' + \\documentclass{article} + \\usepackage{graphicx} + \\begin{document} + \\includegraphics{lion.png} + \\end{document} + ''' + }, @image_resource = { + path: "lion.png" + url: "http://#{host}:31415/#{@file}" + modified: @last_modified = Date.now() + }] + + Client.compile @project_id, @request, (@error, @res, @body) => + sinon.spy Server, "getFile" + @image_resource.modified = new Date(@last_modified - 3000) + Client.compile @project_id, @request, (@error, @res, @body) => + done() + + afterEach -> + Server.getFile.restore() + + it "should not download the image again", -> + Server.getFile.called.should.equal false + + describe "When an image is in the cache and the last modified date is not specified", -> + before (done) -> + @project_id = Client.randomId() + @file = "#{Server.randomId()}/lion.png" + @request = + resources: [{ + path: "main.tex" + content: ''' + \\documentclass{article} + \\usepackage{graphicx} + \\begin{document} + \\includegraphics{lion.png} + \\end{document} + ''' + }, @image_resource = { + path: "lion.png" + url: "http://#{host}:31415/#{@file}" + modified: @last_modified = Date.now() + }] + + Client.compile @project_id, @request, (@error, @res, @body) => + sinon.spy Server, "getFile" + delete @image_resource.modified + Client.compile @project_id, @request, (@error, @res, @body) => + done() + + afterEach -> + Server.getFile.restore() + + it "should download the image again", -> + Server.getFile.called.should.equal true -# describe "After clearing the cache", -> -# before (done) -> -# @project_id = Client.randomId() -# @file = "#{Server.randomId()}/lion.png" -# @request = -# resources: [{ -# path: "main.tex" -# content: ''' -# \\documentclass{article} -# \\usepackage{graphicx} -# \\begin{document} -# \\includegraphics{lion.png} -# \\end{document} -# ''' -# }, @image_resource = { -# path: "lion.png" -# url: "http://#{host}:31415/#{@file}" -# modified: @last_modified = Date.now() -# }] - -# Client.compile @project_id, @request, (error) => -# throw error if error? -# Client.clearCache @project_id, (error, res, body) => -# throw error if error? -# sinon.spy Server, "getFile" -# Client.compile @project_id, @request, (@error, @res, @body) => -# done() - -# afterEach -> -# Server.getFile.restore() - -# it "should download the image again", -> -# Server.getFile.called.should.equal true + describe "After clearing the cache", -> + before (done) -> + @project_id = Client.randomId() + @file = "#{Server.randomId()}/lion.png" + @request = + resources: [{ + path: "main.tex" + content: ''' + \\documentclass{article} + \\usepackage{graphicx} + \\begin{document} + \\includegraphics{lion.png} + \\end{document} + ''' + }, @image_resource = { + path: "lion.png" + url: "http://#{host}:31415/#{@file}" + modified: @last_modified = Date.now() + }] + + Client.compile @project_id, @request, (error) => + throw error if error? + Client.clearCache @project_id, (error, res, body) => + throw error if error? + sinon.spy Server, "getFile" + Client.compile @project_id, @request, (@error, @res, @body) => + done() + + afterEach -> + Server.getFile.restore() + + it "should download the image again", -> + Server.getFile.called.should.equal true diff --git a/test/acceptance/coffee/WordcountTests.coffee b/test/acceptance/coffee/WordcountTests.coffee index c41ba9ac..abace066 100644 --- a/test/acceptance/coffee/WordcountTests.coffee +++ b/test/acceptance/coffee/WordcountTests.coffee @@ -1,38 +1,38 @@ -# Client = require "./helpers/Client" -# request = require "request" -# require("chai").should() -# expect = require("chai").expect -# path = require("path") -# fs = require("fs") -# ClsiApp = require "./helpers/ClsiApp" +Client = require "./helpers/Client" +request = require "request" +require("chai").should() +expect = require("chai").expect +path = require("path") +fs = require("fs") +ClsiApp = require "./helpers/ClsiApp" -# describe "Syncing", -> -# before (done) -> -# @request = -# resources: [ -# path: "main.tex" -# content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") -# ] -# @project_id = Client.randomId() -# ClsiApp.ensureRunning => -# Client.compile @project_id, @request, (@error, @res, @body) => done() +describe "Syncing", -> + before (done) -> + @request = + resources: [ + path: "main.tex" + content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") + ] + @project_id = Client.randomId() + ClsiApp.ensureRunning => + Client.compile @project_id, @request, (@error, @res, @body) => done() -# describe "wordcount file", -> -# it "should return wordcount info", (done) -> -# Client.wordcount @project_id, "main.tex", (error, result) -> -# throw error if error? -# expect(result).to.deep.equal( -# texcount: { -# encode: "utf8" -# textWords: 2281 -# headWords: 2 -# outside: 0 -# headers: 2 -# elements: 0 -# mathInline: 6 -# mathDisplay: 0 -# errors: 0 -# messages: "" -# } -# ) -# done() + describe "wordcount file", -> + it "should return wordcount info", (done) -> + Client.wordcount @project_id, "main.tex", (error, result) -> + throw error if error? + expect(result).to.deep.equal( + texcount: { + encode: "utf8" + textWords: 2281 + headWords: 2 + outside: 0 + headers: 2 + elements: 0 + mathInline: 6 + mathDisplay: 0 + errors: 0 + messages: "" + } + ) + done() From b9874b5ae5839d8aeabf2df170da1ea59d86b79f Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 2 Mar 2018 18:08:13 +0000 Subject: [PATCH 291/709] built with 1.1.0 scripts --- Dockerfile | 4 ++-- docker-compose.yml | 28 ---------------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/Dockerfile b/Dockerfile index f3172014..33873948 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,9 +19,9 @@ WORKDIR /app # All app and node_modules will be owned by root. # The app will run as the 'app' user, and so not have write permissions # on any files it doesn't need. -RUN useradd --user-group --create-home --home-dir /app --shell /bin/bash app +RUN useradd --user-group --create-home --home-dir /app --shell /bin/false app RUN [ -e ./install_deps.sh ] && ./install_deps.sh -# USER app +USER app CMD ["node","app.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 0ed0b544..cf907427 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,34 +29,6 @@ services: - mongo entrypoint: npm run test:acceptance - synctex: - image: quay.io/sharelatex/texlive-full:2017.1 - volumes: - - ~/Projects/sharelatex-dev-environment/clsi/compiles/cd749215b3512:/compile - - ./bin/synctex:/opt/synctex - - entrypoint: - /opt/synctex pdf /compile/output.pdf 1 100 200 - # /opt/synctex code -h - # /opt/synctex code /compile/main.tex ./main.tex 3 5 - # ls -al - - app: - build: . - volumes: - - .:/app - working_dir: /app - extends: - file: docker-compose-config.yml - service: dev - environment: - REDIS_HOST: redis - MONGO_HOST: mongo - depends_on: - - redis - - mongo - - redis: image: redis From 4dd11f3442ecf84dbd3cf816d2ff29fd61994eaf Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Sat, 3 Mar 2018 13:36:42 +0000 Subject: [PATCH 292/709] update docker compose ci to use extension file and dockerfile --- docker-compose.ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index cbe15fe9..2ca2f9a3 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -11,7 +11,11 @@ services: entrypoint: npm run test:unit:_run test_acceptance: + build: . image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + extends: + file: docker-compose-config.yml + service: dev environment: REDIS_HOST: redis MONGO_HOST: mongo From 3134b8aadae5f5eca0f118bd25e550381b9fbca1 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Sat, 3 Mar 2018 13:40:29 +0000 Subject: [PATCH 293/709] add SYNCTEX_BIN_HOST_PATH for ci --- docker-compose-config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose-config.yml b/docker-compose-config.yml index 16897c4d..6e1fca45 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -21,6 +21,7 @@ services: SHARELATEX_CONFIG: /app/config/settings.defaults.coffee COMMAND_RUNNER: docker-runner-sharelatex COMPILES_HOST_DIR: $PWD/compiles + SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex volumes: - /var/run/docker.sock:/var/run/docker.sock:rw - ./docker-runner:/app/node_modules/docker-runner-sharelatex From 3bdd50a231b28833b42061dca386521cd7e93f2c Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 5 Mar 2018 10:39:46 +0000 Subject: [PATCH 294/709] fix url fetcher tests so they exit correctly --- test/unit/coffee/UrlFetcherTests.coffee | 33 ++++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/test/unit/coffee/UrlFetcherTests.coffee b/test/unit/coffee/UrlFetcherTests.coffee index dd709dde..4bd161bc 100644 --- a/test/unit/coffee/UrlFetcherTests.coffee +++ b/test/unit/coffee/UrlFetcherTests.coffee @@ -17,8 +17,8 @@ describe "UrlFetcher", -> @defaults.calledWith(jar: false) .should.equal true - describe "_pipeUrlToFile", -> - beforeEach -> + describe "pipeUrlToFile", -> + beforeEach (done)-> @path = "/path/to/file/on/disk" @request.get = sinon.stub().returns(@urlStream = new EventEmitter) @urlStream.pipe = sinon.stub() @@ -26,21 +26,24 @@ describe "UrlFetcher", -> @urlStream.resume = sinon.stub() @fs.createWriteStream = sinon.stub().returns(@fileStream = new EventEmitter) @fs.unlink = (file, callback) -> callback() - @UrlFetcher.pipeUrlToFile(@url, @path, @callback) - - it "should request the URL", -> - @request.get - .calledWith(sinon.match {"url": @url}) - .should.equal true - + done() describe "successfully", -> - beforeEach -> + beforeEach (done)-> + @UrlFetcher.pipeUrlToFile @url, @path, => + @callback() + done() @res = statusCode: 200 @urlStream.emit "response", @res @urlStream.emit "end" @fileStream.emit "finish" + + it "should request the URL", -> + @request.get + .calledWith(sinon.match {"url": @url}) + .should.equal true + it "should open the file for writing", -> @fs.createWriteStream .calledWith(@path) @@ -55,7 +58,10 @@ describe "UrlFetcher", -> @callback.called.should.equal true describe "with non success status code", -> - beforeEach -> + beforeEach (done)-> + @UrlFetcher.pipeUrlToFile @url, @path, (err)=> + @callback(err) + done() @res = statusCode: 404 @urlStream.emit "response", @res @urlStream.emit "end" @@ -66,7 +72,10 @@ describe "UrlFetcher", -> .should.equal true describe "with error", -> - beforeEach -> + beforeEach (done)-> + @UrlFetcher.pipeUrlToFile @url, @path, (err)=> + @callback(err) + done() @urlStream.emit "error", @error = new Error("something went wrong") it "should call the callback with the error", -> From b202af3cf236e3b3d78503aa97412a7b10536ac2 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 5 Mar 2018 11:02:31 +0000 Subject: [PATCH 295/709] added docker runner into core codebase supports both local command runner and docker runner added docker files for tex live also fixed tests so they exit correctly & removed debug lines --- app/coffee/CommandRunner.coffee | 49 +- app/coffee/CompileManager.coffee | 5 +- app/coffee/DockerLockManager.coffee | 56 ++ app/coffee/DockerRunner.coffee | 348 ++++++++++++ app/coffee/LatexRunner.coffee | 2 +- app/coffee/LocalCommandRunner.coffee | 44 ++ config/settings.defaults.coffee | 4 +- docker-compose-config.yml | 6 +- package.json | 2 +- .../coffee/SimpleLatexFileTests.coffee | 1 - test/acceptance/coffee/TimeoutTests.coffee | 1 - test/acceptance/coffee/helpers/Client.coffee | 1 - test/unit/coffee/CompileManagerTests.coffee | 2 - .../unit/coffee/DockerLockManagerTests.coffee | 145 +++++ test/unit/coffee/DockerRunnerTests.coffee | 499 ++++++++++++++++++ ...Manager.coffee => LockManagerTests.coffee} | 2 +- 16 files changed, 1108 insertions(+), 59 deletions(-) create mode 100644 app/coffee/DockerLockManager.coffee create mode 100644 app/coffee/DockerRunner.coffee create mode 100644 app/coffee/LocalCommandRunner.coffee create mode 100644 test/unit/coffee/DockerLockManagerTests.coffee create mode 100644 test/unit/coffee/DockerRunnerTests.coffee rename test/unit/coffee/{LockManager.coffee => LockManagerTests.coffee} (98%) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index f47af001..2d1c3a94 100644 --- a/app/coffee/CommandRunner.coffee +++ b/app/coffee/CommandRunner.coffee @@ -1,44 +1,11 @@ -spawn = require("child_process").spawn +Settings = require "settings-sharelatex" logger = require "logger-sharelatex" -logger.info "using standard command runner" +if Settings.clsi?.dockerRunner == true + commandRunnerPath = "./DockerRunner" +else + commandRunnerPath = "./LocalCommandRunner" +logger.info commandRunnerPath:commandRunnerPath, "selecting command runner for clsi" +CommandRunner = require(commandRunnerPath) -module.exports = CommandRunner = - 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, env: env - - proc.on "error", (err)-> - logger.err err:err, project_id:project_id, command: command, directory: directory, "error running command" - callback(err) - - 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 if code is 1 # exit status from chktex - err = new Error("exited") - err.code = code - 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() +module.exports = CommandRunner diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 0db15e52..807b8c44 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -15,10 +15,7 @@ fse = require "fs-extra" os = require("os") async = require "async" Errors = require './Errors' - -commandRunner = Settings.clsi?.commandRunner or "./CommandRunner" -logger.info commandRunner:commandRunner, "selecting command runner for clsi" -CommandRunner = require(commandRunner) +CommandRunner = require "./CommandRunner" getCompileName = (project_id, user_id) -> if user_id? then "#{project_id}-#{user_id}" else project_id diff --git a/app/coffee/DockerLockManager.coffee b/app/coffee/DockerLockManager.coffee new file mode 100644 index 00000000..739f2cd1 --- /dev/null +++ b/app/coffee/DockerLockManager.coffee @@ -0,0 +1,56 @@ +logger = require "logger-sharelatex" + +LockState = {} # locks for docker container operations, by container name + +module.exports = LockManager = + + MAX_LOCK_HOLD_TIME: 15000 # how long we can keep a lock + MAX_LOCK_WAIT_TIME: 10000 # how long we wait for a lock + LOCK_TEST_INTERVAL: 1000 # retry time + + tryLock: (key, callback = (err, gotLock) ->) -> + existingLock = LockState[key] + if existingLock? # the lock is already taken, check how old it is + lockAge = Date.now() - existingLock.created + if lockAge < LockManager.MAX_LOCK_HOLD_TIME + return callback(null, false) # we didn't get the lock, bail out + else + logger.error {key: key, lock: existingLock, age:lockAge}, "taking old lock by force" + # take the lock + LockState[key] = lockValue = {created: Date.now()} + callback(null, true, lockValue) + + getLock: (key, callback = (error, lockValue) ->) -> + startTime = Date.now() + do attempt = () -> + LockManager.tryLock key, (error, gotLock, lockValue) -> + return callback(error) if error? + if gotLock + callback(null, lockValue) + else if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME + e = new Error("Lock timeout") + e.key = key + return callback(e) + else + setTimeout attempt, LockManager.LOCK_TEST_INTERVAL + + releaseLock: (key, lockValue, callback = (error) ->) -> + existingLock = LockState[key] + if existingLock is lockValue # lockValue is an object, so we can test by reference + delete LockState[key] # our lock, so we can free it + callback() + else if existingLock? # lock exists but doesn't match ours + logger.error {key:key, lock: existingLock}, "tried to release lock taken by force" + callback() + else + logger.error {key:key, lock: existingLock}, "tried to release lock that has gone" + callback() + + runWithLock: (key, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) -> + LockManager.getLock key, (error, lockValue) -> + return callback(error) if error? + runner (error1, args...) -> + LockManager.releaseLock key, lockValue, (error2) -> + error = error1 or error2 + return callback(error) if error? + callback(null, args...) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee new file mode 100644 index 00000000..f14b5c64 --- /dev/null +++ b/app/coffee/DockerRunner.coffee @@ -0,0 +1,348 @@ +Settings = require "settings-sharelatex" +logger = require "logger-sharelatex" +Docker = require("dockerode") +dockerode = new Docker() +crypto = require "crypto" +async = require "async" +LockManager = require "./DockerLockManager" +fs = require "fs" +Path = require 'path' +_ = require "underscore" + +logger.info "using docker runner" + +usingSiblingContainers = () -> + Settings?.path?.sandboxedCompilesHostDir? + +module.exports = DockerRunner = + ERR_NOT_DIRECTORY: new Error("not a directory") + ERR_TERMINATED: new Error("terminated") + ERR_EXITED: new Error("exited") + ERR_TIMED_OUT: new Error("container timed out") + + run: (project_id, command, directory, image, timeout, environment, callback = (error, output) ->) -> + + if usingSiblingContainers() + _newPath = Settings.path.sandboxedCompilesHostDir + logger.log {path: _newPath}, "altering bind path for sibling containers" + # Server Pro, example: + # '/var/lib/sharelatex/data/compiles/<project-id>' + # ... becomes ... + # '/opt/sharelatex_data/data/compiles/<project-id>' + directory = Path.join(Settings.path.sandboxedCompilesHostDir, Path.basename(directory)) + + volumes = {} + volumes[directory] = "/compile" + + command = (arg.toString().replace?('$COMPILE_DIR', "/compile") for arg in command) + if !image? + image = Settings.clsi.docker.image + + options = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment) + fingerprint = DockerRunner._fingerprintContainer(options) + options.name = name = "project-#{project_id}-#{fingerprint}" + + logger.log project_id: project_id, options: options, "running docker container" + DockerRunner._runAndWaitForContainer options, volumes, timeout, (error, output) -> + if error?.message?.match("HTTP code is 500") + logger.log err: error, project_id: project_id, "error running container so destroying and retrying" + DockerRunner.destroyContainer name, null, true, (error) -> + return callback(error) if error? + DockerRunner._runAndWaitForContainer options, volumes, timeout, callback + else + callback(error, output) + + return name # pass back the container name to allow it to be killed + + kill: (container_id, callback = (error) ->) -> + logger.log container_id: container_id, "sending kill signal to container" + container = dockerode.getContainer(container_id) + container.kill (error) -> + if error? and error?.message?.match?(/Cannot kill container .* is not running/) + logger.warn err: error, container_id: container_id, "container not running, continuing" + error = null + if error? + logger.error err: error, container_id: container_id, "error killing container" + return callback(error) + else + callback() + + _runAndWaitForContainer: (options, volumes, timeout, _callback = (error, output) ->) -> + callback = (args...) -> + _callback(args...) + # Only call the callback once + _callback = () -> + + name = options.name + + streamEnded = false + containerReturned = false + output = {} + + callbackIfFinished = () -> + if streamEnded and containerReturned + callback(null, output) + + attachStreamHandler = (error, _output) -> + return callback(error) if error? + output = _output + streamEnded = true + callbackIfFinished() + + DockerRunner.startContainer options, volumes, attachStreamHandler, (error, containerId) -> + return callback(error) if error? + + DockerRunner.waitForContainer name, timeout, (error, exitCode) -> + return callback(error) if error? + if exitCode is 137 # exit status from kill -9 + err = DockerRunner.ERR_TERMINATED + err.terminated = true + return callback(err) + if exitCode is 1 # exit status from chktex + err = DockerRunner.ERR_EXITED + err.code = exitCode + return callback(err) + containerReturned = true + callbackIfFinished() + + _getContainerOptions: (command, image, volumes, timeout, environment) -> + timeoutInSeconds = timeout / 1000 + + if Settings.path?.synctexBinHostPath? + volumes[Settings.path.synctexBinHostPath] = "/opt/synctex:ro" + + dockerVolumes = {} + for hostVol, dockerVol of volumes + dockerVolumes[dockerVol] = {} + + if volumes[hostVol].slice(-3).indexOf(":r") == -1 + volumes[hostVol] = "#{dockerVol}:rw" + + + # merge settings and environment parameter + env = {} + for src in [Settings.clsi.docker.env, environment or {}] + env[key] = value for key, value of src + # set the path based on the image year + if m = image.match /:([0-9]+)\.[0-9]+/ + year = m[1] + else + year = "2014" + env['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/#{year}/bin/x86_64-linux/" + options = + "Cmd" : command, + "Image" : image + "Volumes" : dockerVolumes + "WorkingDir" : "/compile" + "NetworkDisabled" : true + "Memory" : 1024 * 1024 * 1024 * 1024 # 1 Gb + "User" : Settings.clsi.docker.user + "Env" : ("#{key}=#{value}" for key, value of env) # convert the environment hash to an array + "HostConfig" : + "Binds": ("#{hostVol}:#{dockerVol}" for hostVol, dockerVol of volumes) + "LogConfig": {"Type": "none", "Config": {}} + "Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}] + "CapDrop": "ALL" + "SecurityOpt": ["no-new-privileges"] + if Settings.clsi.docker.seccomp_profile? + options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}" + return options + + _fingerprintContainer: (containerOptions) -> + # Yay, Hashing! + json = JSON.stringify(containerOptions) + return crypto.createHash("md5").update(json).digest("hex") + + startContainer: (options, volumes, attachStreamHandler, callback) -> + LockManager.runWithLock options.name, (releaseLock) -> + # Check that volumes exist before starting the container. + # When a container is started with volume pointing to a + # non-existent directory then docker creates the directory but + # with root ownership. + DockerRunner._checkVolumes options, volumes, (err) -> + return releaseLock(err) if err? + DockerRunner._startContainer options, volumes, attachStreamHandler, releaseLock + , callback + + # Check that volumes exist and are directories + _checkVolumes: (options, volumes, callback = (error, containerName) ->) -> + if usingSiblingContainers() + # Server Pro, with sibling-containers active, skip checks + return callback(null) + + checkVolume = (path, cb) -> + fs.stat path, (err, stats) -> + return cb(err) if err? + return cb(DockerRunner.ERR_NOT_DIRECTORY) if not stats?.isDirectory() + cb() + jobs = [] + for vol of volumes + do (vol) -> + jobs.push (cb) -> checkVolume(vol, cb) + async.series jobs, callback + + _startContainer: (options, volumes, attachStreamHandler, callback = ((error, output) ->)) -> + callback = _.once(callback) + name = options.name + + logger.log {container_name: name}, "starting container" + container = dockerode.getContainer(name) + + createAndStartContainer = -> + dockerode.createContainer options, (error, container) -> + return callback(error) if error? + startExistingContainer() + + startExistingContainer = -> + DockerRunner.attachToContainer options.name, attachStreamHandler, (error)-> + return callback(error) if error? + container.start (error) -> + if error? and error?.statusCode != 304 #already running + return callback(error) + else + callback() + + container.inspect (error, stats)-> + if error?.statusCode == 404 + createAndStartContainer() + else if error? + logger.err {container_name: name}, "unable to inspect container to start" + return callback(error) + else + startExistingContainer() + + + attachToContainer: (containerId, attachStreamHandler, attachStartCallback) -> + container = dockerode.getContainer(containerId) + container.attach {stdout: 1, stderr: 1, stream: 1}, (error, stream) -> + if error? + logger.error err: error, container_id: containerId, "error attaching to container" + return attachStartCallback(error) + else + attachStartCallback() + + + logger.log container_id: containerId, "attached to container" + + MAX_OUTPUT = 1024 * 1024 # limit output to 1MB + createStringOutputStream = (name) -> + return { + data: "" + overflowed: false + write: (data) -> + return if @overflowed + if @data.length < MAX_OUTPUT + @data += data + else + logger.error container_id: containerId, length: @data.length, maxLen: MAX_OUTPUT, "#{name} exceeds max size" + @data += "(...truncated at #{MAX_OUTPUT} chars...)" + @overflowed = true + # kill container if too much output + # docker.containers.kill(containerId, () ->) + } + + stdout = createStringOutputStream "stdout" + stderr = createStringOutputStream "stderr" + + container.modem.demuxStream(stream, stdout, stderr) + + stream.on "error", (err) -> + logger.error err: err, container_id: containerId, "error reading from container stream" + + stream.on "end", () -> + attachStreamHandler null, {stdout: stdout.data, stderr: stderr.data} + + waitForContainer: (containerId, timeout, _callback = (error, exitCode) ->) -> + callback = (args...) -> + _callback(args...) + # Only call the callback once + _callback = () -> + + container = dockerode.getContainer(containerId) + + timedOut = false + timeoutId = setTimeout () -> + timedOut = true + logger.log container_id: containerId, "timeout reached, killing container" + container.kill(() ->) + , timeout + + logger.log container_id: containerId, "waiting for docker container" + container.wait (error, res) -> + if error? + clearTimeout timeoutId + logger.error err: error, container_id: containerId, "error waiting for container" + return callback(error) + if timedOut + logger.log containerId: containerId, "docker container timed out" + error = DockerRunner.ERR_TIMED_OUT + error.timedout = true + callback error + else + clearTimeout timeoutId + logger.log container_id: containerId, exitCode: res.StatusCode, "docker container returned" + callback null, res.StatusCode + + destroyContainer: (containerName, containerId, shouldForce, callback = (error) ->) -> + # We want the containerName for the lock and, ideally, the + # containerId to delete. There is a bug in the docker.io module + # where if you delete by name and there is an error, it throws an + # async exception, but if you delete by id it just does a normal + # error callback. We fall back to deleting by name if no id is + # supplied. + LockManager.runWithLock containerName, (releaseLock) -> + DockerRunner._destroyContainer containerId or containerName, shouldForce, releaseLock + , callback + + _destroyContainer: (containerId, shouldForce, callback = (error) ->) -> + logger.log container_id: containerId, "destroying docker container" + container = dockerode.getContainer(containerId) + container.remove {force: shouldForce == true}, (error) -> + if error? and error?.statusCode == 404 + logger.warn err: error, container_id: containerId, "container not found, continuing" + error = null + if error? + logger.error err: error, container_id: containerId, "error destroying container" + else + logger.log container_id: containerId, "destroyed container" + callback(error) + + # handle expiry of docker containers + + MAX_CONTAINER_AGE: Settings.clsi.docker.maxContainerAge or oneHour = 60 * 60 * 1000 + + examineOldContainer: (container, callback = (error, name, id, ttl)->) -> + name = container.Name or container.Names?[0] + created = container.Created * 1000 # creation time is returned in seconds + now = Date.now() + age = now - created + maxAge = DockerRunner.MAX_CONTAINER_AGE + ttl = maxAge - age + logger.log {containerName: name, created: created, now: now, age: age, maxAge: maxAge, ttl: ttl}, "checking whether to destroy container" + callback(null, name, container.Id, ttl) + + destroyOldContainers: (callback = (error) ->) -> + dockerode.listContainers all: true, (error, containers) -> + return callback(error) if error? + jobs = [] + for container in containers or [] + do (container) -> + DockerRunner.examineOldContainer container, (err, name, id, ttl) -> + if name.slice(0, 9) == '/project-' && ttl <= 0 + jobs.push (cb) -> + DockerRunner.destroyContainer name, id, false, () -> cb() + # Ignore errors because some containers get stuck but + # will be destroyed next time + async.series jobs, callback + + startContainerMonitor: () -> + logger.log {maxAge: DockerRunner.MAX_CONTAINER_AGE}, "starting container expiry" + # randomise the start time + randomDelay = Math.floor(Math.random() * 5 * 60 * 1000) + setTimeout () -> + setInterval () -> + DockerRunner.destroyOldContainers() + , oneHour = 60 * 60 * 1000 + , randomDelay + +DockerRunner.startContainerMonitor() \ No newline at end of file diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 6a5a4f6a..3571af20 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -2,7 +2,7 @@ Path = require "path" Settings = require "settings-sharelatex" logger = require "logger-sharelatex" Metrics = require "./Metrics" -CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner") +CommandRunner = require "./CommandRunner" ProcessTable = {} # table of currently running jobs (pids or docker container names) diff --git a/app/coffee/LocalCommandRunner.coffee b/app/coffee/LocalCommandRunner.coffee new file mode 100644 index 00000000..f47af001 --- /dev/null +++ b/app/coffee/LocalCommandRunner.coffee @@ -0,0 +1,44 @@ +spawn = require("child_process").spawn +logger = require "logger-sharelatex" + +logger.info "using standard command runner" + +module.exports = CommandRunner = + 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, env: env + + proc.on "error", (err)-> + logger.err err:err, project_id:project_id, command: command, directory: directory, "error running command" + callback(err) + + 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 if code is 1 # exit status from chktex + err = new Error("exited") + err.code = code + 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/config/settings.defaults.coffee b/config/settings.defaults.coffee index c29829ee..c76b2517 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -29,9 +29,9 @@ module.exports = project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 -if process.env["COMMAND_RUNNER"] +if process.env["DOCKER_RUNNER"] module.exports.clsi = - commandRunner: process.env["COMMAND_RUNNER"] + dockerRunner: process.env["DOCKER_RUNNER"] == "true" docker: image: process.env["TEXLIVE_IMAGE"] or "quay.io/sharelatex/texlive-full:2017.1" env: diff --git a/docker-compose-config.yml b/docker-compose-config.yml index 6e1fca45..55a3f012 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -6,12 +6,11 @@ services: TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: root SHARELATEX_CONFIG: /app/config/settings.defaults.coffee - COMMAND_RUNNER: docker-runner-sharelatex + DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./docker-runner:/app/node_modules/docker-runner-sharelatex - ./compiles:/app/compiles ci: @@ -19,10 +18,9 @@ services: TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: root SHARELATEX_CONFIG: /app/config/settings.defaults.coffee - COMMAND_RUNNER: docker-runner-sharelatex + DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex volumes: - /var/run/docker.sock:/var/run/docker.sock:rw - - ./docker-runner:/app/node_modules/docker-runner-sharelatex - ./compiles:/app/compiles \ No newline at end of file diff --git a/package.json b/package.json index ae379558..6c5361cb 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", - "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", diff --git a/test/acceptance/coffee/SimpleLatexFileTests.coffee b/test/acceptance/coffee/SimpleLatexFileTests.coffee index 0d3337ad..95b667ba 100644 --- a/test/acceptance/coffee/SimpleLatexFileTests.coffee +++ b/test/acceptance/coffee/SimpleLatexFileTests.coffee @@ -21,7 +21,6 @@ describe "Simple LaTeX file", -> it "should return the PDF", -> pdf = Client.getOutputFile(@body, "pdf") - console.log @body pdf.type.should.equal "pdf" it "should return the log", -> diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index 9195474a..877223a9 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -23,7 +23,6 @@ describe "Timed out compile", -> Client.compile @project_id, @request, (@error, @res, @body) => done() it "should return a timeout error", -> - console.log @body.compile, "!!!1111" @body.compile.error.should.equal "container timed out" it "should return a timedout status", -> diff --git a/test/acceptance/coffee/helpers/Client.coffee b/test/acceptance/coffee/helpers/Client.coffee index 546c2351..39131703 100644 --- a/test/acceptance/coffee/helpers/Client.coffee +++ b/test/acceptance/coffee/helpers/Client.coffee @@ -11,7 +11,6 @@ module.exports = Client = Math.random().toString(16).slice(2) compile: (project_id, data, callback = (error, res, body) ->) -> - console.log("#{@host}/project/#{project_id}/compile") request.post { url: "#{@host}/project/#{project_id}/compile" json: diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 448e06c0..1836c05c 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -293,8 +293,6 @@ describe "CompileManager", -> # .should.equal true it "should call the callback with the parsed output", -> - console.log(@file_name, @line, @column) - console.log @callback.args[0] @callback .calledWith(null, [{ file: @file_name diff --git a/test/unit/coffee/DockerLockManagerTests.coffee b/test/unit/coffee/DockerLockManagerTests.coffee new file mode 100644 index 00000000..6161bec3 --- /dev/null +++ b/test/unit/coffee/DockerLockManagerTests.coffee @@ -0,0 +1,145 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +require "coffee-script" +modulePath = require('path').join __dirname, '../../../app/coffee/DockerLockManager' + +describe "LockManager", -> + beforeEach -> + @LockManager = SandboxedModule.require modulePath, requires: + "settings-sharelatex": @Settings = + clsi: docker: {} + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + + describe "runWithLock", -> + describe "with a single lock", -> + beforeEach (done) -> + @callback = sinon.stub() + @LockManager.runWithLock "lock-one", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world") + , 100 + , (err, args...) => + @callback(err,args...) + done() + + it "should call the callback", -> + @callback.calledWith(null,"hello","world").should.equal true + + describe "with two locks", -> + beforeEach (done) -> + @callback1 = sinon.stub() + @callback2 = sinon.stub() + @LockManager.runWithLock "lock-one", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","one") + , 100 + , (err, args...) => + @callback1(err,args...) + @LockManager.runWithLock "lock-two", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","two") + , 200 + , (err, args...) => + @callback2(err,args...) + done() + + it "should call the first callback", -> + @callback1.calledWith(null,"hello","world","one").should.equal true + + it "should call the second callback", -> + @callback2.calledWith(null,"hello","world","two").should.equal true + + describe "with lock contention", -> + describe "where the first lock is released quickly", -> + beforeEach (done) -> + @LockManager.MAX_LOCK_WAIT_TIME = 1000 + @LockManager.LOCK_TEST_INTERVAL = 100 + @callback1 = sinon.stub() + @callback2 = sinon.stub() + @LockManager.runWithLock "lock", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","one") + , 100 + , (err, args...) => + @callback1(err,args...) + @LockManager.runWithLock "lock", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","two") + , 200 + , (err, args...) => + @callback2(err,args...) + done() + + it "should call the first callback", -> + @callback1.calledWith(null,"hello","world","one").should.equal true + + it "should call the second callback", -> + @callback2.calledWith(null,"hello","world","two").should.equal true + + describe "where the first lock is held longer than the waiting time", -> + beforeEach (done) -> + @LockManager.MAX_LOCK_HOLD_TIME = 10000 + @LockManager.MAX_LOCK_WAIT_TIME = 1000 + @LockManager.LOCK_TEST_INTERVAL = 100 + @callback1 = sinon.stub() + @callback2 = sinon.stub() + doneOne = doneTwo = false + finish = (key) -> + doneOne = true if key is 1 + doneTwo = true if key is 2 + done() if doneOne and doneTwo + @LockManager.runWithLock "lock", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","one") + , 1100 + , (err, args...) => + @callback1(err,args...) + finish(1) + @LockManager.runWithLock "lock", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","two") + , 100 + , (err, args...) => + @callback2(err,args...) + finish(2) + + it "should call the first callback", -> + @callback1.calledWith(null,"hello","world","one").should.equal true + + it "should call the second callback with an error", -> + error = sinon.match.instanceOf Error + @callback2.calledWith(error).should.equal true + + describe "where the first lock is held longer than the max holding time", -> + beforeEach (done) -> + @LockManager.MAX_LOCK_HOLD_TIME = 1000 + @LockManager.MAX_LOCK_WAIT_TIME = 2000 + @LockManager.LOCK_TEST_INTERVAL = 100 + @callback1 = sinon.stub() + @callback2 = sinon.stub() + doneOne = doneTwo = false + finish = (key) -> + doneOne = true if key is 1 + doneTwo = true if key is 2 + done() if doneOne and doneTwo + @LockManager.runWithLock "lock", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","one") + , 1500 + , (err, args...) => + @callback1(err,args...) + finish(1) + @LockManager.runWithLock "lock", (releaseLock) -> + setTimeout () -> + releaseLock(null, "hello", "world","two") + , 100 + , (err, args...) => + @callback2(err,args...) + finish(2) + + it "should call the first callback", -> + @callback1.calledWith(null,"hello","world","one").should.equal true + + it "should call the second callback", -> + @callback2.calledWith(null,"hello","world","two").should.equal true diff --git a/test/unit/coffee/DockerRunnerTests.coffee b/test/unit/coffee/DockerRunnerTests.coffee new file mode 100644 index 00000000..5a697e2b --- /dev/null +++ b/test/unit/coffee/DockerRunnerTests.coffee @@ -0,0 +1,499 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +expect = require('chai').expect +require "coffee-script" +modulePath = require('path').join __dirname, '../../../app/coffee/DockerRunner' +Path = require "path" + +describe "DockerRunner", -> + beforeEach -> + @container = container = {} + @DockerRunner = SandboxedModule.require modulePath, requires: + "settings-sharelatex": @Settings = + clsi: docker: {} + path: {} + "logger-sharelatex": @logger = { + log: sinon.stub(), + error: sinon.stub(), + info: sinon.stub(), + warn: sinon.stub() + } + "dockerode": class Docker + getContainer: sinon.stub().returns(container) + createContainer: sinon.stub().yields(null, container) + listContainers: sinon.stub() + "fs": @fs = { stat: sinon.stub().yields(null,{isDirectory:()->true}) } + "./Metrics": + Timer: class Timer + done: () -> + "./LockManager": + runWithLock: (key, runner, callback) -> runner(callback) + @Docker = Docker + @getContainer = Docker::getContainer + @createContainer = Docker::createContainer + @listContainers = Docker::listContainers + + @directory = "/local/compile/directory" + @mainFile = "main-file.tex" + @compiler = "pdflatex" + @image = "example.com/sharelatex/image:2016.2" + @env = {} + @callback = sinon.stub() + @project_id = "project-id-123" + @volumes = + "/local/compile/directory": "/compile" + @Settings.clsi.docker.image = @defaultImage = "default-image" + @Settings.clsi.docker.env = PATH: "mock-path" + + describe "run", -> + beforeEach (done)-> + @DockerRunner._getContainerOptions = sinon.stub().returns(@options = {mockoptions: "foo"}) + @DockerRunner._fingerprintContainer = sinon.stub().returns(@fingerprint = "fingerprint") + + @name = "project-#{@project_id}-#{@fingerprint}" + + @command = ["mock", "command", "--outdir=$COMPILE_DIR"] + @command_with_dir = ["mock", "command", "--outdir=/compile"] + @timeout = 42000 + done() + + describe "successfully", -> + beforeEach (done)-> + @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") + @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, (err, output)=> + @callback(err, output) + done() + + it "should generate the options for the container", -> + @DockerRunner._getContainerOptions + .calledWith(@command_with_dir, @image, @volumes, @timeout) + .should.equal true + + it "should generate the fingerprint from the returned options", -> + @DockerRunner._fingerprintContainer + .calledWith(@options) + .should.equal true + + it "should do the run", -> + @DockerRunner._runAndWaitForContainer + .calledWith(@options, @volumes, @timeout) + .should.equal true + + it "should call the callback", -> + @callback.calledWith(null, @output).should.equal true + + describe 'when path.sandboxedCompilesHostDir is set', -> + + beforeEach -> + @Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles' + @directory = '/var/lib/sharelatex/data/compiles/xyz' + @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") + @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback + + it 'should re-write the bind directory', -> + volumes = @DockerRunner._runAndWaitForContainer.lastCall.args[1] + expect(volumes).to.deep.equal { + '/some/host/dir/compiles/xyz': '/compile' + } + + it "should call the callback", -> + @callback.calledWith(null, @output).should.equal true + + describe "when the run throws an error", -> + beforeEach -> + firstTime = true + @output = "mock-output" + @DockerRunner._runAndWaitForContainer = (options, volumes, timeout, callback = (error, output)->) => + if firstTime + firstTime = false + callback new Error("HTTP code is 500 which indicates error: server error") + else + callback(null, @output) + sinon.spy @DockerRunner, "_runAndWaitForContainer" + @DockerRunner.destroyContainer = sinon.stub().callsArg(3) + @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback + + it "should do the run twice", -> + @DockerRunner._runAndWaitForContainer + .calledTwice.should.equal true + + it "should destroy the container in between", -> + @DockerRunner.destroyContainer + .calledWith(@name, null) + .should.equal true + + it "should call the callback", -> + @callback.calledWith(null, @output).should.equal true + + describe "with no image", -> + beforeEach -> + @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") + @DockerRunner.run @project_id, @command, @directory, null, @timeout, @env, @callback + + it "should use the default image", -> + @DockerRunner._getContainerOptions + .calledWith(@command_with_dir, @defaultImage, @volumes, @timeout) + .should.equal true + + describe "_runAndWaitForContainer", -> + beforeEach -> + @options = {mockoptions: "foo", name: @name = "mock-name"} + @DockerRunner.startContainer = (options, volumes, attachStreamHandler, callback) => + attachStreamHandler(null, @output = "mock-output") + callback(null, @containerId = "container-id") + sinon.spy @DockerRunner, "startContainer" + @DockerRunner.waitForContainer = sinon.stub().callsArgWith(2, null, @exitCode = 42) + @DockerRunner._runAndWaitForContainer @options, @volumes, @timeout, @callback + + it "should create/start the container", -> + @DockerRunner.startContainer + .calledWith(@options, @volumes) + .should.equal true + + it "should wait for the container to finish", -> + @DockerRunner.waitForContainer + .calledWith(@name, @timeout) + .should.equal true + + it "should call the callback with the output", -> + @callback.calledWith(null, @output).should.equal true + + describe "startContainer", -> + beforeEach -> + @attachStreamHandler = sinon.stub() + @attachStreamHandler.cock = true + @options = {mockoptions: "foo", name: "mock-name"} + @container.inspect = sinon.stub().callsArgWith(0) + @DockerRunner.attachToContainer = (containerId, attachStreamHandler, cb)=> + attachStreamHandler() + cb() + sinon.spy @DockerRunner, "attachToContainer" + + + + describe "when the container exists", -> + beforeEach -> + @container.inspect = sinon.stub().callsArgWith(0) + @container.start = sinon.stub().yields() + + @DockerRunner.startContainer @options, @volumes, @callback, -> + + it "should start the container with the given name", -> + @getContainer + .calledWith(@options.name) + .should.equal true + @container.start + .called + .should.equal true + + it "should not try to create the container", -> + @createContainer.called.should.equal false + + it "should attach to the container", -> + @DockerRunner.attachToContainer.called.should.equal true + + it "should call the callback", -> + @callback.called.should.equal true + + it "should attach before the container starts", -> + sinon.assert.callOrder(@DockerRunner.attachToContainer, @container.start) + + describe "when the container does not exist", -> + beforeEach ()-> + exists = false + @container.start = sinon.stub().yields() + @container.inspect = sinon.stub().callsArgWith(0, {statusCode:404}) + @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback + + it "should create the container", -> + @createContainer + .calledWith(@options) + .should.equal true + + it "should call the callback and stream handler", -> + @attachStreamHandler.called.should.equal true + @callback.called.should.equal true + + it "should attach to the container", -> + @DockerRunner.attachToContainer.called.should.equal true + + it "should attach before the container starts", -> + sinon.assert.callOrder(@DockerRunner.attachToContainer, @container.start) + + + describe "when the container is already running", -> + beforeEach -> + error = new Error("HTTP code is 304 which indicates error: server error - start: Cannot start container #{@name}: The container MOCKID is already running.") + error.statusCode = 304 + @container.start = sinon.stub().yields(error) + @container.inspect = sinon.stub().callsArgWith(0) + @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback + + it "should not try to create the container", -> + @createContainer.called.should.equal false + + it "should call the callback and stream handler without an error", -> + @attachStreamHandler.called.should.equal true + @callback.called.should.equal true + + describe "when a volume does not exist", -> + beforeEach ()-> + @fs.stat = sinon.stub().yields(new Error("no such path")) + @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback + + it "should not try to create the container", -> + @createContainer.called.should.equal false + + it "should call the callback with an error", -> + @callback.calledWith(new Error()).should.equal true + + describe "when a volume exists but is not a directory", -> + beforeEach -> + @fs.stat = sinon.stub().yields(null, {isDirectory: () -> return false}) + @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback + + it "should not try to create the container", -> + @createContainer.called.should.equal false + + it "should call the callback with an error", -> + @callback.calledWith(new Error()).should.equal true + + describe "when a volume does not exist, but sibling-containers are used", -> + beforeEach -> + @fs.stat = sinon.stub().yields(new Error("no such path")) + @Settings.path.sandboxedCompilesHostDir = '/some/path' + @container.start = sinon.stub().yields() + @DockerRunner.startContainer @options, @volumes, @callback + + afterEach -> + delete @Settings.path.sandboxedCompilesHostDir + + it "should start the container with the given name", -> + @getContainer + .calledWith(@options.name) + .should.equal true + @container.start + .called + .should.equal true + + it "should not try to create the container", -> + @createContainer.called.should.equal false + + it "should call the callback", -> + @callback.called.should.equal true + @callback.calledWith(new Error()).should.equal false + + describe "when the container tries to be created, but already has been (race condition)", -> + + describe "waitForContainer", -> + beforeEach -> + @containerId = "container-id" + @timeout = 5000 + @container.wait = sinon.stub().yields(null, StatusCode: @statusCode = 42) + @container.kill = sinon.stub().yields() + + describe "when the container returns in time", -> + beforeEach -> + @DockerRunner.waitForContainer @containerId, @timeout, @callback + + it "should wait for the container", -> + @getContainer + .calledWith(@containerId) + .should.equal true + @container.wait + .called + .should.equal true + + it "should call the callback with the exit", -> + @callback + .calledWith(null, @statusCode) + .should.equal true + + describe "when the container does not return before the timeout", -> + beforeEach (done) -> + @container.wait = (callback = (error, exitCode) ->) -> + setTimeout () -> + callback(null, StatusCode: 42) + , 100 + @timeout = 5 + @DockerRunner.waitForContainer @containerId, @timeout, (args...) => + @callback(args...) + done() + + it "should call kill on the container", -> + @getContainer + .calledWith(@containerId) + .should.equal true + @container.kill + .called + .should.equal true + + it "should call the callback with an error", -> + error = new Error("container timed out") + error.timedout = true + @callback + .calledWith(error) + .should.equal true + + describe "destroyOldContainers", -> + beforeEach (done) -> + oneHourInSeconds = 60 * 60 + oneHourInMilliseconds = oneHourInSeconds * 1000 + nowInSeconds = Date.now()/1000 + @containers = [{ + Name: "/project-old-container-name" + Id: "old-container-id" + Created: nowInSeconds - oneHourInSeconds - 100 + }, { + Name: "/project-new-container-name" + Id: "new-container-id" + Created: nowInSeconds - oneHourInSeconds + 100 + }, { + Name: "/totally-not-a-project-container" + Id: "some-random-id" + Created: nowInSeconds - (2 * oneHourInSeconds ) + }] + @DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds + @listContainers.callsArgWith(1, null, @containers) + @DockerRunner.destroyContainer = sinon.stub().callsArg(3) + @DockerRunner.destroyOldContainers (error) => + @callback(error) + done() + + it "should list all containers", -> + @listContainers + .calledWith(all: true) + .should.equal true + + it "should destroy old containers", -> + @DockerRunner.destroyContainer + .callCount + .should.equal 1 + @DockerRunner.destroyContainer + .calledWith("/project-old-container-name", "old-container-id") + .should.equal true + + it "should not destroy new containers", -> + @DockerRunner.destroyContainer + .calledWith("/project-new-container-name", "new-container-id") + .should.equal false + + it "should not destroy non-project containers", -> + @DockerRunner.destroyContainer + .calledWith("/totally-not-a-project-container", "some-random-id") + .should.equal false + + it "should callback the callback", -> + @callback.called.should.equal true + + + describe '_destroyContainer', -> + beforeEach -> + @containerId = 'some_id' + @fakeContainer = + remove: sinon.stub().callsArgWith(1, null) + @Docker::getContainer = sinon.stub().returns(@fakeContainer) + + it 'should get the container', (done) -> + @DockerRunner._destroyContainer @containerId, false, (err) => + @Docker::getContainer.callCount.should.equal 1 + @Docker::getContainer.calledWith(@containerId).should.equal true + done() + + it 'should try to force-destroy the container when shouldForce=true', (done) -> + @DockerRunner._destroyContainer @containerId, true, (err) => + @fakeContainer.remove.callCount.should.equal 1 + @fakeContainer.remove.calledWith({force: true}).should.equal true + done() + + it 'should not try to force-destroy the container when shouldForce=false', (done) -> + @DockerRunner._destroyContainer @containerId, false, (err) => + @fakeContainer.remove.callCount.should.equal 1 + @fakeContainer.remove.calledWith({force: false}).should.equal true + done() + + it 'should not produce an error', (done) -> + @DockerRunner._destroyContainer @containerId, false, (err) => + expect(err).to.equal null + done() + + describe 'when the container is already gone', -> + beforeEach -> + @fakeError = new Error('woops') + @fakeError.statusCode = 404 + @fakeContainer = + remove: sinon.stub().callsArgWith(1, @fakeError) + @Docker::getContainer = sinon.stub().returns(@fakeContainer) + + it 'should not produce an error', (done) -> + @DockerRunner._destroyContainer @containerId, false, (err) => + expect(err).to.equal null + done() + + describe 'when container.destroy produces an error', (done) -> + beforeEach -> + @fakeError = new Error('woops') + @fakeError.statusCode = 500 + @fakeContainer = + remove: sinon.stub().callsArgWith(1, @fakeError) + @Docker::getContainer = sinon.stub().returns(@fakeContainer) + + it 'should produce an error', (done) -> + @DockerRunner._destroyContainer @containerId, false, (err) => + expect(err).to.not.equal null + expect(err).to.equal @fakeError + done() + + + describe 'kill', -> + beforeEach -> + @containerId = 'some_id' + @fakeContainer = + kill: sinon.stub().callsArgWith(0, null) + @Docker::getContainer = sinon.stub().returns(@fakeContainer) + + it 'should get the container', (done) -> + @DockerRunner.kill @containerId, (err) => + @Docker::getContainer.callCount.should.equal 1 + @Docker::getContainer.calledWith(@containerId).should.equal true + done() + + it 'should try to force-destroy the container', (done) -> + @DockerRunner.kill @containerId, (err) => + @fakeContainer.kill.callCount.should.equal 1 + done() + + it 'should not produce an error', (done) -> + @DockerRunner.kill @containerId, (err) => + expect(err).to.equal undefined + done() + + describe 'when the container is not actually running', -> + beforeEach -> + @fakeError = new Error('woops') + @fakeError.statusCode = 500 + @fakeError.message = 'Cannot kill container <whatever> is not running' + @fakeContainer = + kill: sinon.stub().callsArgWith(0, @fakeError) + @Docker::getContainer = sinon.stub().returns(@fakeContainer) + + it 'should not produce an error', (done) -> + @DockerRunner.kill @containerId, (err) => + expect(err).to.equal undefined + done() + + describe 'when container.kill produces a legitimate error', (done) -> + beforeEach -> + @fakeError = new Error('woops') + @fakeError.statusCode = 500 + @fakeError.message = 'Totally legitimate reason to throw an error' + @fakeContainer = + kill: sinon.stub().callsArgWith(0, @fakeError) + @Docker::getContainer = sinon.stub().returns(@fakeContainer) + + it 'should produce an error', (done) -> + @DockerRunner.kill @containerId, (err) => + expect(err).to.not.equal undefined + expect(err).to.equal @fakeError + done() diff --git a/test/unit/coffee/LockManager.coffee b/test/unit/coffee/LockManagerTests.coffee similarity index 98% rename from test/unit/coffee/LockManager.coffee rename to test/unit/coffee/LockManagerTests.coffee index c1071a58..2d0c95af 100644 --- a/test/unit/coffee/LockManager.coffee +++ b/test/unit/coffee/LockManagerTests.coffee @@ -5,7 +5,7 @@ modulePath = require('path').join __dirname, '../../../app/js/LockManager' Path = require "path" Errors = require "../../../app/js/Errors" -describe "LockManager", -> +describe "DockerLockManager", -> beforeEach -> @LockManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": {} From f1df41112b0127b56fd3912e96263539cfe76875 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 5 Mar 2018 15:29:12 +0000 Subject: [PATCH 296/709] wip for ci --- Jenkinsfile | 2 +- config/settings.defaults.coffee | 2 +- docker-compose-config.yml | 1 + docker-compose.ci.yml | 2 +- install_deps.sh | 2 ++ 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index bc9ba014..8607d393 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -23,7 +23,7 @@ pipeline { stage('Acceptance Tests') { steps { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_acceptance' + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" test_acceptance' } } diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index c76b2517..3100a5d7 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -9,7 +9,7 @@ module.exports = username: "clsi" password: null dialect: "sqlite" - storage: Path.resolve(__dirname + "/../db.sqlite") + storage: process.env["SQLITE_PATH"] or Path.resolve(__dirname + "/../db.sqlite") path: compilesDir: Path.resolve(__dirname + "/../compiles") diff --git a/docker-compose-config.yml b/docker-compose-config.yml index 55a3f012..f2bb8631 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -21,6 +21,7 @@ services: DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex + SQLITE_PATH: /app/compiles/db.sqlite volumes: - /var/run/docker.sock:/var/run/docker.sock:rw - ./compiles:/app/compiles \ No newline at end of file diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 2ca2f9a3..98715c26 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -15,7 +15,7 @@ services: image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER extends: file: docker-compose-config.yml - service: dev + service: ci environment: REDIS_HOST: redis MONGO_HOST: mongo diff --git a/install_deps.sh b/install_deps.sh index bde142a5..dce17a4c 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -6,3 +6,5 @@ usermod -aG docker app touch /var/run/docker.sock chown root:docker /var/run/docker.sock + +chown -R app:app /app/cache From 2f96350b7cf31b549f02c54f9877f94ba0399e02 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 5 Mar 2018 15:30:15 +0000 Subject: [PATCH 297/709] removed unused scripts --- package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/package.json b/package.json index 6c5361cb..771ee96d 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,13 @@ "url": "https://github.com/sharelatex/clsi-sharelatex.git" }, "scripts": { - "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3013/status) do sleep 1; done", - "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000", - "test:acceptance:dir": "npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", - "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", From 177c46df985b7d194cf7b5771fa166212530d401 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 5 Mar 2018 15:35:08 +0000 Subject: [PATCH 298/709] add cache dir --- install_deps.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install_deps.sh b/install_deps.sh index dce17a4c..078c3117 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -7,4 +7,5 @@ usermod -aG docker app touch /var/run/docker.sock chown root:docker /var/run/docker.sock +mkdir -p /app/cache chown -R app:app /app/cache From 00cf5468d090a86f428067ce10703ccfbd01827c Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 5 Mar 2018 15:40:33 +0000 Subject: [PATCH 299/709] update jenkins task --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8607d393..993d22f0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -23,7 +23,7 @@ pipeline { stage('Acceptance Tests') { steps { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" test_acceptance' + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" up --build test_acceptance' } } From 4e6514b17e6cbf97dc90f53098369fc4bf4c264d Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 5 Mar 2018 16:02:16 +0000 Subject: [PATCH 300/709] add logging in db.coffee --- app/coffee/db.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index a72f61e8..cb633592 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -1,6 +1,7 @@ Sequelize = require("sequelize") Settings = require("settings-sharelatex") _ = require("underscore") +logger = require "logger-sharelatex" options = _.extend {logging:false}, Settings.mysql.clsi @@ -32,5 +33,7 @@ module.exports = ] }) - sync: () -> sequelize.sync() + sync: () -> + logger.log dbPath:Settings.mysql.clsi.storage, "syncing db schema" + sequelize.sync() From 96a237fb749e1dfe9d9d264f4f2d66b73f51b17c Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 6 Mar 2018 10:30:05 +0000 Subject: [PATCH 301/709] removed user temporarly, created make ci task --- Dockerfile | 2 +- Jenkinsfile | 22 ++-------------------- Makefile | 12 ++++++++++++ docker-compose.ci.yml | 9 +-------- docker-compose.yml | 6 ------ 5 files changed, 16 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index 33873948..58266614 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,5 +23,5 @@ RUN useradd --user-group --create-home --home-dir /app --shell /bin/false app RUN [ -e ./install_deps.sh ] && ./install_deps.sh -USER app +# USER app CMD ["node","app.js"] diff --git a/Jenkinsfile b/Jenkinsfile index 993d22f0..e92cfbe5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,27 +9,9 @@ pipeline { } stages { - stage('Build') { + stage('CI') { steps { - sh 'make build' - } - } - - stage('Unit Tests') { - steps { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit' - } - } - - stage('Acceptance Tests') { - steps { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" up --build test_acceptance' - } - } - - stage('Package and publish build') { - steps { - sh 'make publish' + sh 'make ci' } } diff --git a/Makefile b/Makefile index b20654c6..450efc80 100644 --- a/Makefile +++ b/Makefile @@ -28,10 +28,22 @@ test_acceptance: test_clean # clear the database before each acceptance test run test_clean: $(DOCKER_COMPOSE) down -t 0 + build: docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . publish: docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) +ci: + # On the CI server, we want to run our tests in the image that we + # have built for deployment, which is what the docker-compose.ci.yml + # override does. + PROJECT_NAME=$(PROJECT_NAME) \ + BRANCH_NAME=$(BRANCH_NAME) \ + BUILD_NUMBER=$(BUILD_NUMBER) \ + DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" \ + $(MAKE) build test + + .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 98715c26..16a5da36 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -12,17 +12,10 @@ services: test_acceptance: build: . - image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER extends: file: docker-compose-config.yml service: ci - environment: - REDIS_HOST: redis - MONGO_HOST: mongo - depends_on: - - redis - - mongo - entrypoint: npm run test:acceptance:_run + entrypoint: npm run test:acceptance redis: image: redis diff --git a/docker-compose.yml b/docker-compose.yml index cf907427..2469482b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,12 +21,6 @@ services: extends: file: docker-compose-config.yml service: dev - environment: - REDIS_HOST: redis - MONGO_HOST: mongo - depends_on: - - redis - - mongo entrypoint: npm run test:acceptance redis: From aca9100c52a5c63511d013eaf8dbfe44f0d143bd Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 6 Mar 2018 10:59:40 +0000 Subject: [PATCH 302/709] set entry point for dockerfile --- Dockerfile | 3 +-- docker-compose.yml | 2 +- entrypoint.sh | 5 +++++ 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100755 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 58266614..db863cda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,5 +23,4 @@ RUN useradd --user-group --create-home --home-dir /app --shell /bin/false app RUN [ -e ./install_deps.sh ] && ./install_deps.sh -# USER app -CMD ["node","app.js"] +ENTRYPOINT ["/bin/sh", "entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index 2469482b..e6af23b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: extends: file: docker-compose-config.yml service: dev - entrypoint: npm run test:acceptance + command: npm run test:acceptance redis: image: redis diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..588854ed --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo "Changing permissions of /var/run/docker.sock for sibling containers" +chown root:docker /var/run/docker.sock +exec runuser -u app "$@" \ No newline at end of file From 4ff11213531fea8c21c1fdb63f83d7d81b13c7b8 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 6 Mar 2018 11:26:43 +0000 Subject: [PATCH 303/709] add cmd back in --- Dockerfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index db863cda..de5f2182 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,6 @@ WORKDIR /app RUN npm install - RUN npm run compile:all FROM node:6.13.0 @@ -15,12 +14,9 @@ COPY --from=app /app /app WORKDIR /app - -# All app and node_modules will be owned by root. -# The app will run as the 'app' user, and so not have write permissions -# on any files it doesn't need. RUN useradd --user-group --create-home --home-dir /app --shell /bin/false app RUN [ -e ./install_deps.sh ] && ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] +CMD ["node","app.js"] From 3c4870f68850f77e4a855392c9d84af60c0287f3 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 6 Mar 2018 11:27:51 +0000 Subject: [PATCH 304/709] =?UTF-8?q?remove=20touch=20/var/run/docker.sock?= =?UTF-8?q?=20which=20doesn=E2=80=99t=20work=20robustly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install_deps.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/install_deps.sh b/install_deps.sh index 078c3117..18839499 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -4,8 +4,5 @@ apt-get install poppler-utils vim ghostscript --yes npm rebuild usermod -aG docker app -touch /var/run/docker.sock -chown root:docker /var/run/docker.sock - mkdir -p /app/cache chown -R app:app /app/cache From 7dbed15fea2d74f1b5c7f438e6e82c12e834a71f Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 6 Mar 2018 12:07:54 +0000 Subject: [PATCH 305/709] update scripts from latest build scripts 1.1.0 --- Dockerfile | 7 ++++++- Jenkinsfile | 1 - Makefile | 5 ++--- docker-compose.ci.yml | 11 +++++++++-- docker-compose.yml | 8 +++++++- package.json | 2 +- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index de5f2182..62ef3425 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ WORKDIR /app RUN npm install + RUN npm run compile:all FROM node:6.13.0 @@ -14,9 +15,13 @@ COPY --from=app /app /app WORKDIR /app + +# All app and node_modules will be owned by root. +# The app will run as the 'app' user, and so not have write permissions +# on any files it doesn't need. RUN useradd --user-group --create-home --home-dir /app --shell /bin/false app RUN [ -e ./install_deps.sh ] && ./install_deps.sh - ENTRYPOINT ["/bin/sh", "entrypoint.sh"] + CMD ["node","app.js"] diff --git a/Jenkinsfile b/Jenkinsfile index e92cfbe5..7a34ee75 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,7 +14,6 @@ pipeline { sh 'make ci' } } - stage('Publish build number') { steps { sh 'echo ${BRANCH_NAME}-${BUILD_NUMBER} > build_number.txt' diff --git a/Makefile b/Makefile index 450efc80..063d96e6 100644 --- a/Makefile +++ b/Makefile @@ -28,13 +28,12 @@ test_acceptance: test_clean # clear the database before each acceptance test run test_clean: $(DOCKER_COMPOSE) down -t 0 - build: docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . publish: docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - + ci: # On the CI server, we want to run our tests in the image that we # have built for deployment, which is what the docker-compose.ci.yml @@ -43,7 +42,7 @@ ci: BRANCH_NAME=$(BRANCH_NAME) \ BUILD_NUMBER=$(BUILD_NUMBER) \ DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" \ - $(MAKE) build test + $(MAKE) build test publish .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 16a5da36..2ca2f9a3 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -12,10 +12,17 @@ services: test_acceptance: build: . + image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER extends: file: docker-compose-config.yml - service: ci - entrypoint: npm run test:acceptance + service: dev + environment: + REDIS_HOST: redis + MONGO_HOST: mongo + depends_on: + - redis + - mongo + entrypoint: npm run test:acceptance:_run redis: image: redis diff --git a/docker-compose.yml b/docker-compose.yml index e6af23b1..cf907427 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,13 @@ services: extends: file: docker-compose-config.yml service: dev - command: npm run test:acceptance + environment: + REDIS_HOST: redis + MONGO_HOST: mongo + depends_on: + - redis + - mongo + entrypoint: npm run test:acceptance redis: image: redis diff --git a/package.json b/package.json index 771ee96d..fed32605 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", "nodemon": "nodemon --config nodemon.json" }, - "author": "James Allen <james@sharelatex.com>", + "author": "James Allen <james@sharelatex.com>", "dependencies": { "async": "0.2.9", "body-parser": "^1.2.0", From b9d94fb428c08cf8828da271b7074497033a1043 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 6 Mar 2018 13:06:10 +0000 Subject: [PATCH 306/709] fixed commended tests --- test/unit/coffee/CompileManagerTests.coffee | 37 +++++++++++++-------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 1836c05c..93b7f11b 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -259,13 +259,19 @@ describe "CompileManager", -> @CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout}) @CompileManager.syncFromCode @project_id, @user_id, @file_name, @line, @column, @callback - # it "should execute the synctex binary", -> - # bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - # synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" - # file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}" - # @child_process.execFile - # .calledWith(bin_path, ["code", synctex_path, file_path, @line, @column], timeout: 10000) - # .should.equal true + it "should execute the synctex binary", -> + bin_path = Path.resolve(__dirname + "/../../../bin/synctex") + synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" + file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}" + @CommandRunner.run + .calledWith( + "#{@project_id}-#{@user_id}", + ['/opt/synctex', 'code', synctex_path, file_path, @line, @column], + "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}", + @Settings.clsi.docker.image, + 10000, + {} + ).should.equal true it "should call the callback with the parsed output", -> @callback @@ -285,12 +291,17 @@ describe "CompileManager", -> @CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout}) @CompileManager.syncFromPdf @project_id, @user_id, @page, @h, @v, @callback - # it "should execute the synctex binary", -> - # bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - # synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" - # @CommandRunner.run - # .calledWith(bin_path, ["pdf", synctex_path, @page, @h, @v], timeout: 10000) - # .should.equal true + it "should execute the synctex binary", -> + bin_path = Path.resolve(__dirname + "/../../../bin/synctex") + synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" + @CommandRunner.run + .calledWith( + "#{@project_id}-#{@user_id}", + ['/opt/synctex', "pdf", synctex_path, @page, @h, @v], + "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}", + @Settings.clsi.docker.image, + 10000, + {}).should.equal true it "should call the callback with the parsed output", -> @callback From 83c7068bd1e07672d9b0acd859947d1b1d41db72 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 6 Mar 2018 16:03:26 +0000 Subject: [PATCH 307/709] test new scripts on ci --- Jenkinsfile | 23 +++++++++++++++++++++-- Makefile | 10 ---------- docker-compose.ci.yml | 2 +- package.json | 4 ++-- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7a34ee75..bc9ba014 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,11 +9,30 @@ pipeline { } stages { - stage('CI') { + stage('Build') { steps { - sh 'make ci' + sh 'make build' } } + + stage('Unit Tests') { + steps { + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit' + } + } + + stage('Acceptance Tests') { + steps { + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_acceptance' + } + } + + stage('Package and publish build') { + steps { + sh 'make publish' + } + } + stage('Publish build number') { steps { sh 'echo ${BRANCH_NAME}-${BUILD_NUMBER} > build_number.txt' diff --git a/Makefile b/Makefile index 063d96e6..74b78a3d 100644 --- a/Makefile +++ b/Makefile @@ -33,16 +33,6 @@ build: publish: docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - -ci: - # On the CI server, we want to run our tests in the image that we - # have built for deployment, which is what the docker-compose.ci.yml - # override does. - PROJECT_NAME=$(PROJECT_NAME) \ - BRANCH_NAME=$(BRANCH_NAME) \ - BUILD_NUMBER=$(BUILD_NUMBER) \ - DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" \ - $(MAKE) build test publish .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 2ca2f9a3..98715c26 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -15,7 +15,7 @@ services: image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER extends: file: docker-compose-config.yml - service: dev + service: ci environment: REDIS_HOST: redis MONGO_HOST: mongo diff --git a/package.json b/package.json index fed32605..54181da5 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "scripts": { "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", - "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", + "compile:app": "coffee $COFFEE_OPTIONS -o app/js -c app/coffee && coffee $COFFEE_OPTIONS -c app.coffee", "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", - "start": "npm run compile:app && node app.js", + "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", From dc3cb439d004bf5f172ee90b4ca92251aa9f9740 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 7 Mar 2018 11:23:52 +0000 Subject: [PATCH 308/709] update build scripts --- Dockerfile | 5 +++-- docker-compose.ci.yml | 4 ++-- docker-compose.yml | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 62ef3425..ba3b3e1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,8 @@ COPY ./ /app WORKDIR /app +RUN rm -rf node_modules/* && make clean + RUN npm install @@ -20,8 +22,7 @@ WORKDIR /app # The app will run as the 'app' user, and so not have write permissions # on any files it doesn't need. RUN useradd --user-group --create-home --home-dir /app --shell /bin/false app - -RUN [ -e ./install_deps.sh ] && ./install_deps.sh +RUN ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] CMD ["node","app.js"] diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 98715c26..85512cf7 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -8,7 +8,7 @@ version: "2" services: test_unit: image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER - entrypoint: npm run test:unit:_run + command: npm run test:unit:_run test_acceptance: build: . @@ -22,7 +22,7 @@ services: depends_on: - redis - mongo - entrypoint: npm run test:acceptance:_run + command: npm run test:acceptance:_run redis: image: redis diff --git a/docker-compose.yml b/docker-compose.yml index cf907427..18506876 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: volumes: - .:/app working_dir: /app - entrypoint: npm run test:unit + command: npm run test:unit test_acceptance: build: . @@ -27,7 +27,7 @@ services: depends_on: - redis - mongo - entrypoint: npm run test:acceptance + command: npm run test:acceptance redis: image: redis From 0c1b699bd525fdafdfb8befd3ff14bd56a69d435 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 7 Mar 2018 11:41:38 +0000 Subject: [PATCH 309/709] add docker ignore rather than make clean --- .dockerignore | 3 +++ Dockerfile | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a9983535 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules/* +app.js +**/js/* diff --git a/Dockerfile b/Dockerfile index ba3b3e1b..94247abf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,6 @@ COPY ./ /app WORKDIR /app -RUN rm -rf node_modules/* && make clean - RUN npm install From a741a238a8f2ac6007055e33fcedce1be28a0b74 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 8 Mar 2018 15:38:17 +0000 Subject: [PATCH 310/709] have entrypoint kickoff download off texlive images install script exits without error if auth fails. --- Makefile | 2 +- bin/install_texlive_gce.sh | 13 +++++++++++++ entrypoint.sh | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100755 bin/install_texlive_gce.sh diff --git a/Makefile b/Makefile index 74b78a3d..5c46c268 100644 --- a/Makefile +++ b/Makefile @@ -28,11 +28,11 @@ test_acceptance: test_clean # clear the database before each acceptance test run test_clean: $(DOCKER_COMPOSE) down -t 0 + build: docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . publish: docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/bin/install_texlive_gce.sh b/bin/install_texlive_gce.sh new file mode 100755 index 00000000..20d2ca07 --- /dev/null +++ b/bin/install_texlive_gce.sh @@ -0,0 +1,13 @@ +#!/bin/sh +METADATA=http://metadata.google.internal./computeMetadata/v1 +SVC_ACCT=$METADATA/instance/service-accounts/default +ACCESS_TOKEN=$(curl -s -H 'Metadata-Flavor: Google' $SVC_ACCT/token | cut -d'"' -f 4) +if [ -z "$ACCESS_TOKEN" ]; then + echo "No acccess token to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." + exit 0 +fi +docker login -u '_token' -p $ACCESS_TOKEN https://gcr.io +docker pull --all-tags gcr.io/henry-terraform-admin/texlive-full #TODO NEED TO MAKE THIS AN ENV VAR +echo "Finished downloading texlive-full images" + + diff --git a/entrypoint.sh b/entrypoint.sh index 588854ed..f4861a44 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,4 +2,5 @@ echo "Changing permissions of /var/run/docker.sock for sibling containers" chown root:docker /var/run/docker.sock +./bin/install_texlive_gce.sh exec runuser -u app "$@" \ No newline at end of file From 52982b8fcd8d856e1312504c5772c4df9a02fe41 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 14 Mar 2018 15:20:27 +0000 Subject: [PATCH 311/709] remove texlive docker images --- docker-runner | 1 - package.json | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) delete mode 160000 docker-runner diff --git a/docker-runner b/docker-runner deleted file mode 160000 index 65ad6211..00000000 --- a/docker-runner +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 65ad62116fb1ba4fca978a6d1d6b89bf195e5166 diff --git a/package.json b/package.json index 54181da5..fdb50307 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,6 @@ "url": "https://github.com/sharelatex/clsi-sharelatex.git" }, "scripts": { - "compile:test:acceptance": "coffee -o test/acceptance/js -c test/acceptance/coffee", - "compile:test:smoke": "coffee -o test/smoke/js -c test/smoke/coffee", "compile:app": "coffee $COFFEE_OPTIONS -o app/js -c app/coffee && coffee $COFFEE_OPTIONS -c app.coffee", "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", @@ -16,10 +14,11 @@ "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", - "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", - "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", - "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", - "nodemon": "nodemon --config nodemon.json" + "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile, skipping'", + "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile, skipping'", + "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests", + "nodemon": "nodemon --config nodemon.json", + "compile:smoke_tests": "[ -e test/smoke ] && coffee -o test/smoke/js -c test/smoke/coffee || echo 'No smoke tests to compile, skipping'" }, "author": "James Allen <james@sharelatex.com>", "dependencies": { From 1dce40c61fbdd6a82df69ab9a8bcbd3b567c9958 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 15:25:36 +0000 Subject: [PATCH 312/709] make compiles dir --- install_deps.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install_deps.sh b/install_deps.sh index 18839499..511f291c 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -6,3 +6,6 @@ usermod -aG docker app mkdir -p /app/cache chown -R app:app /app/cache + +mkdir -p /app/compiles +chown -R app:app /app/compiles From 9f8a68be3811db3e3c82c530c7195ff650c10956 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 15:29:35 +0000 Subject: [PATCH 313/709] add log line for connecting to a db --- app/coffee/db.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index cb633592..f32cdf7a 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -5,6 +5,8 @@ logger = require "logger-sharelatex" options = _.extend {logging:false}, Settings.mysql.clsi +logger.log dbPath:Settings.mysql.clsi.storage, "connecting to db" + sequelize = new Sequelize( Settings.mysql.clsi.database, Settings.mysql.clsi.username, From 5739a2aeca85a1da1a179fe25eb2f0f108ae743e Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 16:04:26 +0000 Subject: [PATCH 314/709] comment out synctex for moment --- app/coffee/DockerRunner.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index f14b5c64..3ee7f293 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -108,8 +108,8 @@ module.exports = DockerRunner = _getContainerOptions: (command, image, volumes, timeout, environment) -> timeoutInSeconds = timeout / 1000 - if Settings.path?.synctexBinHostPath? - volumes[Settings.path.synctexBinHostPath] = "/opt/synctex:ro" + # if Settings.path?.synctexBinHostPath? + # volumes[Settings.path.synctexBinHostPath] = "/opt/synctex:ro" dockerVolumes = {} for hostVol, dockerVol of volumes From 63145cc60c347ea0bc6416950ce4c010e56c1329 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 16:22:39 +0000 Subject: [PATCH 315/709] add synctex back in --- app/coffee/DockerRunner.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 3ee7f293..f14b5c64 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -108,8 +108,8 @@ module.exports = DockerRunner = _getContainerOptions: (command, image, volumes, timeout, environment) -> timeoutInSeconds = timeout / 1000 - # if Settings.path?.synctexBinHostPath? - # volumes[Settings.path.synctexBinHostPath] = "/opt/synctex:ro" + if Settings.path?.synctexBinHostPath? + volumes[Settings.path.synctexBinHostPath] = "/opt/synctex:ro" dockerVolumes = {} for hostVol, dockerVol of volumes From 6fbfcfc68b0fd48d66ae2fd97da905674662b33d Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 16:50:30 +0000 Subject: [PATCH 316/709] move synctex into a directory for simple mounting --- .gitignore | 1 - bin/{ => synctex}/synctex | Bin 2 files changed, 1 deletion(-) rename bin/{ => synctex}/synctex (100%) diff --git a/.gitignore b/.gitignore index 476f2d1f..21c1ecae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,4 @@ cache .vagrant db.sqlite config/* -bin/synctex npm-debug.log diff --git a/bin/synctex b/bin/synctex/synctex similarity index 100% rename from bin/synctex rename to bin/synctex/synctex From f4226ecd0ed128ab8b531d273982ccd5dc4a2b51 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 17:10:56 +0000 Subject: [PATCH 317/709] try copying synctex betwen directories --- bin/{synctex => }/synctex | Bin entrypoint.sh | 4 ++++ 2 files changed, 4 insertions(+) rename bin/{synctex => }/synctex (100%) diff --git a/bin/synctex/synctex b/bin/synctex similarity index 100% rename from bin/synctex/synctex rename to bin/synctex diff --git a/entrypoint.sh b/entrypoint.sh index f4861a44..d50a70a3 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/sh echo "Changing permissions of /var/run/docker.sock for sibling containers" +cp /var/clsi/bin/synctex /var/clsi/bin/synctex-mount/synctex + chown root:docker /var/run/docker.sock +chown app:app /app/compiles + ./bin/install_texlive_gce.sh exec runuser -u app "$@" \ No newline at end of file From 17c51c2ba0bd2556e8a9d4f3eaf11e75ff9b5c1f Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 17:30:11 +0000 Subject: [PATCH 318/709] added debugging and new moving commands --- app.coffee | 4 ++++ entrypoint.sh | 8 ++++++-- install_deps.sh | 5 +---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app.coffee b/app.coffee index 527b17e9..7bd05646 100644 --- a/app.coffee +++ b/app.coffee @@ -144,6 +144,10 @@ app.get "/smoke_test_force", (req, res)-> smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res) +#TODO delete this +app.get "/settings", (req, res)-> + res.json(Settings) + profiler = require "v8-profiler" app.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") diff --git a/entrypoint.sh b/entrypoint.sh index d50a70a3..ecb28d11 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,10 +1,14 @@ #!/bin/sh echo "Changing permissions of /var/run/docker.sock for sibling containers" -cp /var/clsi/bin/synctex /var/clsi/bin/synctex-mount/synctex chown root:docker /var/run/docker.sock -chown app:app /app/compiles + +mkdir -p /app/cache +chown -R app:app /app/cache + +mkdir -p /app/compiles +chown -R app:app /app/compiles ./bin/install_texlive_gce.sh exec runuser -u app "$@" \ No newline at end of file diff --git a/install_deps.sh b/install_deps.sh index 511f291c..20ab9465 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -4,8 +4,5 @@ apt-get install poppler-utils vim ghostscript --yes npm rebuild usermod -aG docker app -mkdir -p /app/cache -chown -R app:app /app/cache +cp /app/bin/synctex /app/bin/synctex-mount/synctex -mkdir -p /app/compiles -chown -R app:app /app/compiles From 3d9a93ad617ba116556409cace1f62dd2b5b0701 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 17:37:36 +0000 Subject: [PATCH 319/709] add logging of docker options --- app/coffee/DockerRunner.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index f14b5c64..8323d6c4 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -146,6 +146,7 @@ module.exports = DockerRunner = "SecurityOpt": ["no-new-privileges"] if Settings.clsi.docker.seccomp_profile? options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}" + logger.log options:options, "options for running docker container" return options _fingerprintContainer: (containerOptions) -> From 3c1d7ab264b4369aae4e969e0d27358ba458b25f Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 17:40:10 +0000 Subject: [PATCH 320/709] mkdir the /app/bin/synctex-mount --- install_deps.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install_deps.sh b/install_deps.sh index 20ab9465..76aaf090 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -4,5 +4,6 @@ apt-get install poppler-utils vim ghostscript --yes npm rebuild usermod -aG docker app +mkdir /app/bin/synctex-mount cp /app/bin/synctex /app/bin/synctex-mount/synctex From 0bd937701802df43b294ceb129f0fffea6623abb Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 17:48:55 +0000 Subject: [PATCH 321/709] chown synctex and add the creation of directories in --- bin/install_texlive_gce.sh | 1 + install_deps.sh | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/install_texlive_gce.sh b/bin/install_texlive_gce.sh index 20d2ca07..2ea5f99b 100755 --- a/bin/install_texlive_gce.sh +++ b/bin/install_texlive_gce.sh @@ -8,6 +8,7 @@ if [ -z "$ACCESS_TOKEN" ]; then fi docker login -u '_token' -p $ACCESS_TOKEN https://gcr.io docker pull --all-tags gcr.io/henry-terraform-admin/texlive-full #TODO NEED TO MAKE THIS AN ENV VAR +cp /app/bin/synctex /app/bin/synctex-mount/synctex echo "Finished downloading texlive-full images" diff --git a/install_deps.sh b/install_deps.sh index 76aaf090..e1f3ce46 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -4,6 +4,10 @@ apt-get install poppler-utils vim ghostscript --yes npm rebuild usermod -aG docker app -mkdir /app/bin/synctex-mount -cp /app/bin/synctex /app/bin/synctex-mount/synctex +mkdir -p /app/cache +chown -R app:app /app/cache +mkdir -p /app/compiles +chown -R app:app /app/compiles + +chown -R app:app /app/bin/synctex From 8ccbfc7d3263c7ae442806707140830760d00f03 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 16 Mar 2018 18:11:46 +0000 Subject: [PATCH 322/709] don't put synctex in as a volume --- app/coffee/DockerRunner.coffee | 9 +++++---- bin/install_texlive_gce.sh | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 8323d6c4..635243fd 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -108,9 +108,6 @@ module.exports = DockerRunner = _getContainerOptions: (command, image, volumes, timeout, environment) -> timeoutInSeconds = timeout / 1000 - if Settings.path?.synctexBinHostPath? - volumes[Settings.path.synctexBinHostPath] = "/opt/synctex:ro" - dockerVolumes = {} for hostVol, dockerVol of volumes dockerVolumes[dockerVol] = {} @@ -118,7 +115,6 @@ module.exports = DockerRunner = if volumes[hostVol].slice(-3).indexOf(":r") == -1 volumes[hostVol] = "#{dockerVol}:rw" - # merge settings and environment parameter env = {} for src in [Settings.clsi.docker.env, environment or {}] @@ -144,8 +140,13 @@ module.exports = DockerRunner = "Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}] "CapDrop": "ALL" "SecurityOpt": ["no-new-privileges"] + if Settings.clsi.docker.seccomp_profile? options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}" + + if Settings.path?.synctexBinHostPath? + options["HostConfig"]["Binds"].push("#{Settings.path.synctexBinHostPath}:/opt/synctex:ro") + logger.log options:options, "options for running docker container" return options diff --git a/bin/install_texlive_gce.sh b/bin/install_texlive_gce.sh index 2ea5f99b..85ab7096 100755 --- a/bin/install_texlive_gce.sh +++ b/bin/install_texlive_gce.sh @@ -9,6 +9,7 @@ fi docker login -u '_token' -p $ACCESS_TOKEN https://gcr.io docker pull --all-tags gcr.io/henry-terraform-admin/texlive-full #TODO NEED TO MAKE THIS AN ENV VAR cp /app/bin/synctex /app/bin/synctex-mount/synctex + echo "Finished downloading texlive-full images" From aeb6f48945b68319d0b30dafd581b18c86229f41 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 19 Mar 2018 09:51:26 +0000 Subject: [PATCH 323/709] try running as root --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index ecb28d11..6722a418 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -11,4 +11,4 @@ mkdir -p /app/compiles chown -R app:app /app/compiles ./bin/install_texlive_gce.sh -exec runuser -u app "$@" \ No newline at end of file +exec "$@" \ No newline at end of file From 0915ac8c606728a1e6d38f97494da6455caad83a Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 19 Mar 2018 12:56:53 +0000 Subject: [PATCH 324/709] run as app user and chmod 777 compiles dir --- entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 6722a418..ee1df04b 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -9,6 +9,7 @@ chown -R app:app /app/cache mkdir -p /app/compiles chown -R app:app /app/compiles +chmod -R 777 /app/compiles #TODO why do I need this? ./bin/install_texlive_gce.sh -exec "$@" \ No newline at end of file +exec runuser -u app "$@" \ No newline at end of file From 4d955a8d4144e2917d22be211cc208d2b05924c4 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 19 Mar 2018 14:10:45 +0000 Subject: [PATCH 325/709] try a build with node user --- Dockerfile | 1 - entrypoint.sh | 7 +++---- install_deps.sh | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 94247abf..c30764d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,6 @@ WORKDIR /app # All app and node_modules will be owned by root. # The app will run as the 'app' user, and so not have write permissions # on any files it doesn't need. -RUN useradd --user-group --create-home --home-dir /app --shell /bin/false app RUN ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh index ee1df04b..423a5d28 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,11 +5,10 @@ echo "Changing permissions of /var/run/docker.sock for sibling containers" chown root:docker /var/run/docker.sock mkdir -p /app/cache -chown -R app:app /app/cache +chown -R node:node /app/cache mkdir -p /app/compiles -chown -R app:app /app/compiles -chmod -R 777 /app/compiles #TODO why do I need this? +chown -R node:node /app/compiles ./bin/install_texlive_gce.sh -exec runuser -u app "$@" \ No newline at end of file +exec runuser -u node "$@" \ No newline at end of file diff --git a/install_deps.sh b/install_deps.sh index e1f3ce46..4b06f22d 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -2,12 +2,12 @@ wget -qO- https://get.docker.com/ | sh apt-get install poppler-utils vim ghostscript --yes npm rebuild -usermod -aG docker app +usermod -aG docker node mkdir -p /app/cache -chown -R app:app /app/cache +chown -R node:node /app/cache mkdir -p /app/compiles -chown -R app:app /app/compiles +chown -R node:node /app/compiles -chown -R app:app /app/bin/synctex +chown -R node:node /app/bin/synctex From dc1ea9d3e96223f4ac6e0c860cae672affec8195 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 19 Mar 2018 14:22:18 +0000 Subject: [PATCH 326/709] ammend comment --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c30764d7..68e1a4a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,8 @@ COPY --from=app /app /app WORKDIR /app - # All app and node_modules will be owned by root. -# The app will run as the 'app' user, and so not have write permissions +# The app will run as the 'node' user, and so not have write permissions # on any files it doesn't need. RUN ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] From ec75f9fa6732bea223e5aa91e11cc97386514e15 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 20 Mar 2018 13:48:12 +0000 Subject: [PATCH 327/709] add smoke test env var --- config/settings.defaults.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 3100a5d7..de853f59 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -25,7 +25,7 @@ module.exports = clsi: url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" - smokeTest: false + smokeTest: process.env["SMOKE_TEST"] or false project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 From b5a7eabaab1cb996dfe9c1e6e7ed72e8f0eabf46 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 29 Mar 2018 12:12:29 +0100 Subject: [PATCH 328/709] update build script and add load balancer agent --- .viminfo | 35 + Dockerfile | 4 - Makefile | 6 +- app.coffee | 41 + debug | 5 + docker-compose.ci.yml | 6 +- docker-compose.yml | 6 +- entrypoint.sh | 2 + nodemon.json | 3 +- package-lock.json | 3723 +------------------------------------- package.json | 6 +- patch-texlive-dockerfile | 3 + synctex.profile | 34 + 13 files changed, 215 insertions(+), 3659 deletions(-) create mode 100644 .viminfo create mode 100755 debug create mode 100644 patch-texlive-dockerfile create mode 100644 synctex.profile diff --git a/.viminfo b/.viminfo new file mode 100644 index 00000000..78c01298 --- /dev/null +++ b/.viminfo @@ -0,0 +1,35 @@ +# This viminfo file was generated by Vim 7.4. +# You may edit it if you're careful! + +# Value of 'encoding' when this file was written +*encoding=latin1 + + +# hlsearch on (H) or off (h): +~h +# Command Line History (newest to oldest): +:x + +# Search String History (newest to oldest): + +# Expression History (newest to oldest): + +# Input Line History (newest to oldest): + +# Input Line History (newest to oldest): + +# Registers: + +# File marks: +'0 1 0 ~/hello + +# Jumplist (newest first): +-' 1 0 ~/hello + +# History of marks within files (newest to oldest): + +> ~/hello + " 1 0 + ^ 1 1 + . 1 0 + + 1 0 diff --git a/Dockerfile b/Dockerfile index 68e1a4a9..ed1f2c6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,10 +14,6 @@ FROM node:6.13.0 COPY --from=app /app /app WORKDIR /app - -# All app and node_modules will be owned by root. -# The app will run as the 'node' user, and so not have write permissions -# on any files it doesn't need. RUN ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] diff --git a/Makefile b/Makefile index 5c46c268..2a1196c2 100644 --- a/Makefile +++ b/Makefile @@ -21,13 +21,13 @@ clean: test: test_unit test_acceptance test_unit: - @[ -d test/unit ] && $(DOCKER_COMPOSE) run --rm test_unit -- ${MOCHA_ARGS} || echo "clsi has no unit tests" + @[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit -- ${MOCHA_ARGS} test_acceptance: test_clean # clear the database before each acceptance test run - @[ -d test/acceptance ] && $(DOCKER_COMPOSE) run --rm test_acceptance -- ${MOCHA_ARGS} || echo "clsi has no acceptance tests" + @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance -- ${MOCHA_ARGS} test_clean: - $(DOCKER_COMPOSE) down -t 0 + $(DOCKER_COMPOSE) down -t -v 0 build: docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . diff --git a/app.coffee b/app.coffee index 7bd05646..a3f4ef17 100644 --- a/app.coffee +++ b/app.coffee @@ -169,13 +169,54 @@ app.use (error, req, res, next) -> logger.error {err: error, url: req.url}, "server error" res.sendStatus(error?.statusCode || 500) +net = require "net" +os = require "os" + +STATE = "up" + +server = net.createServer (socket) -> + socket.on "error", (err)-> + if err.code == "ECONNRESET" + # this always comes up, we don't know why + return + logger.err err:err, "error with socket on load check" + socket.destroy() + + if STATE == "up" and settings.load_balancer_agent.report_load + currentLoad = os.loadavg()[0] + + # staging clis's have 1 cpu core only + if os.cpus().length == 1 + availableWorkingCpus = 1 + else + availableWorkingCpus = os.cpus().length - 1 + + freeLoad = availableWorkingCpus - currentLoad + freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) + if freeLoadPercentage <= 0 + freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects to different servers + socket.write("up, #{freeLoadPercentage}%\n", "ASCII") + socket.end() + else + socket.write("#{STATE}\n", "ASCII") + socket.end() + + + + port = (Settings.internal?.clsi?.port or 3013) host = (Settings.internal?.clsi?.host or "localhost") +load_port = settings.internal.clsi.load_port or 3048 + + if !module.parent # Called directly app.listen port, host, (error) -> logger.info "CLSI starting up, listening on #{host}:#{port}" + server.listen load_port, host, (error) -> + throw error if error? + logger.info "Load agent listening on load port #{load_port}" module.exports = app diff --git a/debug b/debug new file mode 100755 index 00000000..fcc371c3 --- /dev/null +++ b/debug @@ -0,0 +1,5 @@ +#!/bin/bash +echo "hello world" +sleep 3 +echo "awake" +/opt/synctex pdf /compile/output.pdf 1 100 200 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 85512cf7..370e2a80 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -8,7 +8,8 @@ version: "2" services: test_unit: image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER - command: npm run test:unit:_run + user: node + entrypoint: npm run test:unit:_run test_acceptance: build: . @@ -22,7 +23,8 @@ services: depends_on: - redis - mongo - command: npm run test:acceptance:_run + user: node + entrypoint: npm run test:acceptance:_run redis: image: redis diff --git a/docker-compose.yml b/docker-compose.yml index 18506876..32cbf61e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,8 @@ services: volumes: - .:/app working_dir: /app - command: npm run test:unit + user: node + entrypoint: npm run test:unit test_acceptance: build: . @@ -24,10 +25,11 @@ services: environment: REDIS_HOST: redis MONGO_HOST: mongo + user: node depends_on: - redis - mongo - command: npm run test:acceptance + entrypoint: npm run test:acceptance redis: image: redis diff --git a/entrypoint.sh b/entrypoint.sh index 423a5d28..ee5ea47f 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -11,4 +11,6 @@ mkdir -p /app/compiles chown -R node:node /app/compiles ./bin/install_texlive_gce.sh +echo "HELOOOo" +echo "$@" exec runuser -u node "$@" \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index 9a3be8d9..815f3922 100644 --- a/nodemon.json +++ b/nodemon.json @@ -10,7 +10,8 @@ }, "watch": [ "app/coffee/", - "app.coffee" + "app.coffee", + "config/" ], "ext": "coffee" } diff --git a/package-lock.json b/package-lock.json index a5b7847c..d00b345e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,70 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "JSONStream": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", - "integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=", - "requires": { - "jsonparse": "0.0.5", - "through": "2.3.8" - } - }, - "async": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", - "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, - "bl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", - "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", - "requires": { - "readable-stream": "2.3.3" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "1.0.0", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "string_decoder": "1.0.3", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - } - } - } - }, - "block-stream": { - "version": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - } - }, "body-parser": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", @@ -224,292 +166,12 @@ "concat-map": "0.0.1" } }, - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "bunyan": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", - "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", - "dev": true, - "requires": { - "mv": "2.1.1" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "optional": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true - }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "optional": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "dev": true, - "optional": true, - "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" - } - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": true - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "dev": true, - "optional": true, - "requires": { - "glob": "6.0.4" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } - }, - "chai": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", - "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", - "dev": true, - "requires": { - "assertion-error": "1.0.0", - "deep-eql": "0.1.3" - }, - "dependencies": { - "assertion-error": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", - "dev": true - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - } - }, - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "coffee-script": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" - }, - "commander": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "optional": true }, - "concat-stream": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "requires": { - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "readable-stream": "2.0.6", - "typedarray": "0.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "1.0.0", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - } - } - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "core-util-is": { - "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "requires": { - "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - } - }, - "diff": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" - }, - "docker-modem": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz", - "integrity": "sha1-JEJUsiax1/YCJfgjlb2FGQz3Ves=", - "requires": { - "JSONStream": "0.10.0", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "readable-stream": "1.0.34", - "split-ca": "1.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - } - } - } - }, - "dockerode": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz", - "integrity": "sha1-Wsw8Bx96JuRDpbWM83U+8vPCvAQ=", - "requires": { - "concat-stream": "1.5.2", - "docker-modem": "1.0.4", - "tar-fs": "1.12.0" - } - }, "dtrace-provider": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", @@ -519,20 +181,6 @@ "nan": "2.8.0" } }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", - "requires": { - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, "express": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", @@ -851,151 +499,52 @@ } } }, - "fs-extra": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", - "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, "requires": { - "graceful-fs": "3.0.11", - "jsonfile": "2.4.0", - "rimraf": "2.6.2" + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "requires": { - "natives": "1.1.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "4.1.11" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "optional": true - } - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "natives": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.1.tgz", - "integrity": "sha512-8eRaxn8u/4wN8tGkhlc2cgwwvOLMLUMUn4IYTexMgWd+LyUDfeXVkk2ygQR0hvIHbJQXgHujia3ieUUDwNGkEA==" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1.0.2" } }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true } } }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", "once": "1.4.0", - "path-is-absolute": "1.0.1" + "wrappy": "1.0.2" }, "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "optional": true - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1008,719 +557,79 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "optional": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" - }, - "growl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" - }, - "grunt": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", - "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", - "dev": true, + "load-balancer-agent-sharelatex": { + "version": "git+https://github.com/sharelatex/load-balancer-agent-sharelatex.git#241a128c1e9fdec384186061b27704c8891d98ef", "requires": { - "async": "0.1.22", - "coffee-script": "1.3.3", - "colors": "0.6.2", - "dateformat": "1.0.2-1.2.3", - "eventemitter2": "0.4.14", - "exit": "0.1.2", - "findup-sync": "0.1.3", - "getobject": "0.1.0", - "glob": "3.1.21", - "grunt-legacy-log": "0.1.3", - "grunt-legacy-util": "0.2.0", - "hooker": "0.2.3", - "iconv-lite": "0.2.11", - "js-yaml": "2.0.5", - "lodash": "0.9.2", - "minimatch": "0.2.14", - "nopt": "1.0.10", - "rimraf": "2.2.8", - "underscore.string": "2.2.1", - "which": "1.0.9" + "express": "4.16.2", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#405cf1350ca5ae5f7bb1e7091e28d5aa3aaaa72c", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#b4fb8404c5de571d029bf4c29e96a60b21206f94" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "argparse": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", - "dev": true, + "bunyan": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", + "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", "requires": { - "underscore": "1.7.0", - "underscore.string": "2.4.0" - }, - "dependencies": { - "underscore.string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", - "dev": true - } + "dtrace-provider": "0.6.0", + "mv": "2.1.1", + "safe-json-stringify": "1.0.4" } }, - "async": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", - "dev": true - }, "coffee-script": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", - "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", - "dev": true - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "dev": true - }, - "dateformat": { - "version": "1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", - "dev": true + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", + "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" }, - "esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", - "dev": true + "cookie": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz", + "integrity": "sha1-kOtGndzpBchm3mh+/EMTHYgB+dA=" }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true + "logger-sharelatex": { + "version": "git+https://github.com/sharelatex/logger-sharelatex.git#405cf1350ca5ae5f7bb1e7091e28d5aa3aaaa72c", + "requires": { + "bunyan": "1.5.1", + "coffee-script": "1.4.0", + "raven": "0.8.1" + } }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true + "lsmod": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz", + "integrity": "sha1-F+E9ThrpF1DqVlNUjNiecUetAkQ=" }, - "findup-sync": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", - "dev": true, + "raven": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz", + "integrity": "sha1-UVk7tlnHcnjc00gitlq+d7dRuvU=", "requires": { - "glob": "3.2.11", - "lodash": "2.4.2" - }, - "dependencies": { - "glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "minimatch": "0.3.0" - } - }, - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "minimatch": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - } + "cookie": "0.1.0", + "lsmod": "0.0.3", + "node-uuid": "1.4.8", + "stack-trace": "0.0.7" } }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", - "dev": true - }, - "glob": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", - "dev": true, + "settings-sharelatex": { + "version": "git+https://github.com/sharelatex/settings-sharelatex.git#b4fb8404c5de571d029bf4c29e96a60b21206f94", "requires": { - "graceful-fs": "1.2.3", - "inherits": "1.0.2", - "minimatch": "0.2.14" + "coffee-script": "1.6.0" }, "dependencies": { - "inherits": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", - "dev": true + "coffee-script": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" } } }, - "graceful-fs": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", - "dev": true - }, - "grunt-legacy-log": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", - "dev": true, - "requires": { - "colors": "0.6.2", - "grunt-legacy-log-utils": "0.1.1", - "hooker": "0.2.3", - "lodash": "2.4.2", - "underscore.string": "2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", - "dev": true - } - } - }, - "grunt-legacy-log-utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", - "dev": true, - "requires": { - "colors": "0.6.2", - "lodash": "2.4.2", - "underscore.string": "2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", - "dev": true - } - } - }, - "grunt-legacy-util": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", - "dev": true, - "requires": { - "async": "0.1.22", - "exit": "0.1.2", - "getobject": "0.1.0", - "hooker": "0.2.3", - "lodash": "0.9.2", - "underscore.string": "2.2.1", - "which": "1.0.9" - } - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true - }, - "iconv-lite": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", - "dev": true - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "js-yaml": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", - "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", - "dev": true, - "requires": { - "argparse": "0.1.16", - "esprima": "1.0.4" - } - }, - "lodash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", - "dev": true - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", - "dev": true - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1.1.1" - } - }, - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - }, - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", - "dev": true - }, - "underscore.string": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", - "dev": true - }, - "which": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", - "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", - "dev": true - } - } - }, - "grunt-bunyan": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", - "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", - "dev": true, - "requires": { - "lodash": "2.4.2" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - } - } - }, - "grunt-contrib-clean": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", - "integrity": "sha1-9T397ghJsce0Dp67umn0jExgecU=", - "dev": true, - "requires": { - "rimraf": "2.2.8" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true - } - } - }, - "grunt-contrib-coffee": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", - "integrity": "sha1-ixIme3TnM4sfKcW4txj7n4mYLxM=", - "dev": true, - "requires": { - "coffee-script": "1.6.3" - }, - "dependencies": { - "coffee-script": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", - "integrity": "sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4=", - "dev": true - } - } - }, - "grunt-execute": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", - "integrity": "sha1-yX64lDYS/vu3L749Mu+VIzxfouk=", - "dev": true - }, - "grunt-mkdir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz", - "integrity": "sha1-c+GiasJKCFljY/TdlUsNMkheWOk=" - }, - "grunt-mocha-test": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", - "integrity": "sha1-emGEuYhg0Phb3qrWvqob199bvus=", - "dev": true, - "requires": { - "mocha": "1.14.0" - }, - "dependencies": { - "commander": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=", - "dev": true - }, - "diff": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", - "dev": true - }, - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "dev": true, - "requires": { - "graceful-fs": "2.0.3", - "inherits": "2.0.3", - "minimatch": "0.2.14" - } - }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", - "dev": true - }, - "growl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", - "dev": true - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "dev": true, - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", - "dev": true - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", - "dev": true - } - } - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", - "dev": true - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "mocha": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", - "integrity": "sha1-cT223FAAGRqdA1gZXQkIeQ7LYVc=", - "dev": true, - "requires": { - "commander": "2.0.0", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "diff": "1.0.7", - "glob": "3.2.3", - "growl": "1.7.0", - "jade": "0.26.3", - "mkdirp": "0.3.5" - } - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - } - } - }, - "grunt-shell": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", - "integrity": "sha1-5KbRuSkSd2/ZOimcX2zGTpUlNlw=", - "dev": true, - "requires": { - "chalk": "0.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", - "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", - "dev": true - }, - "chalk": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", - "integrity": "sha1-HJhDdzfxGZ68wdTEj9Qbn5yOjyM=", - "dev": true, - "requires": { - "ansi-styles": "0.2.0", - "has-color": "0.1.7" - } - }, - "has-color": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", - "dev": true - } - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "heapdump": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.9.tgz", - "integrity": "sha1-A8dOsN9dZ74Jgug0KbqcnSs7f3g=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - }, - "dependencies": { - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "optional": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" - } - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonparse": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", - "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=" - }, - "lockfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.3.tgz", - "integrity": "sha1-Jjj8OaAzHpysGgS3F5mTHJxQ33k=" - }, - "logger-sharelatex": { - "version": "git+https://github.com/sharelatex/logger-sharelatex.git#9ee7b52eb2bbd8fcbb1e2c708587c1e93fd4c733", - "requires": { - "bunyan": "1.5.1", - "coffee-script": "1.4.0", - "raven": "1.2.1" - }, - "dependencies": { - "bunyan": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", - "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", - "requires": { - "dtrace-provider": "0.6.0", - "mv": "2.1.1", - "safe-json-stringify": "1.0.4" - } - }, - "coffee-script": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", - "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" - } - } - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "lsmod": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" - }, - "lynx": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", - "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", - "requires": { - "mersenne": "0.0.4", - "statsd-parser": "0.0.4" - }, - "dependencies": { - "mersenne": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" - }, - "statsd-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", - "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" - } - } - }, - "mersenne": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" - }, - "metrics-sharelatex": { - "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#05bd57604115ae5efdca2a419fbef4f25e9a780f", - "requires": { - "coffee-script": "1.6.0", - "lynx": "0.1.1", - "underscore": "1.6.0" - }, - "dependencies": { - "lynx": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", - "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", - "requires": { - "mersenne": "0.0.4", - "statsd-parser": "0.0.4" - } - }, - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" + "stack-trace": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.7.tgz", + "integrity": "sha1-xy4Il0T8Nln1CM3ONiGvVjTsD/8=" } } }, @@ -1733,253 +642,31 @@ "brace-expansion": "1.1.11" } }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" - }, - "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha1-fYbPvPNcuCnidUwy4XNV7AUzh5Q=", - "dev": true, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", "mkdirp": "0.5.1", - "supports-color": "4.4.0" + "ncp": "2.0.0", + "rimraf": "2.4.5" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha1-GSa6kM8+3+KttJJ/WIC8IsZseQ8=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "optional": true }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "optional": true, - "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, - "requires": { - "minimist": "0.0.8" - } - } - } - }, - "mysql": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", - "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", - "requires": { - "bignumber.js": "2.0.7", - "readable-stream": "1.1.14", - "require-all": "1.0.0" - }, - "dependencies": { - "bignumber.js": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", - "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "require-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", - "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, @@ -2000,419 +687,12 @@ "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" }, - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - } - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, - "process-nextick-args": { - "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha1-Xf6DEcM7v2/BgmH580cCxHwIqVQ=", - "requires": { - "end-of-stream": "1.4.1", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - } - }, - "raven": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", - "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", - "requires": { - "cookie": "0.3.1", - "json-stringify-safe": "5.0.1", - "lsmod": "1.0.0", - "stack-trace": "0.0.9", - "uuid": "3.0.0" - } - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.17" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.0" - } - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha1-EsUou51Y0LkmXZovbw/ovhf/HxQ=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - } - } - }, "rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", @@ -2422,1856 +702,11 @@ "glob": "6.0.4" } }, - "safe-buffer": { - "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" - }, "safe-json-stringify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", "optional": true - }, - "sandboxed-module": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", - "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", - "dev": true, - "requires": { - "require-like": "0.1.2", - "stack-trace": "0.0.6" - }, - "dependencies": { - "require-like": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", - "dev": true - }, - "stack-trace": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", - "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", - "dev": true - } - } - }, - "sequelize": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-2.1.3.tgz", - "integrity": "sha1-FrpROxNnlY/oIzhYEelRFFHt6tE=", - "requires": { - "bluebird": "2.9.34", - "dottie": "0.3.1", - "generic-pool": "2.2.0", - "inflection": "1.12.0", - "lodash": "3.10.1", - "moment": "2.20.1", - "node-uuid": "1.4.8", - "toposort-class": "0.3.1", - "validator": "3.43.0" - }, - "dependencies": { - "bluebird": { - "version": "2.9.34", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz", - "integrity": "sha1-L3tOyAIWMoqf3evfacjUlC/v99g=" - }, - "dottie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-0.3.1.tgz", - "integrity": "sha1-QYX3d0iZYKwIQTHeP5oUoXm7dJ8=" - }, - "generic-pool": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.2.0.tgz", - "integrity": "sha1-i0ZcGnWI6p3SuxM72gu2a/74pj4=" - }, - "inflection": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - }, - "moment": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", - "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" - }, - "toposort-class": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-0.3.1.tgz", - "integrity": "sha1-0TM67XNPhgjO0ShiGL0sRfYO/0g=" - }, - "validator": { - "version": "3.43.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz", - "integrity": "sha1-lkZLmS1BloM9l6GUv0Cxn/VLrgU=" - } - } - }, - "settings-sharelatex": { - "version": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", - "requires": { - "coffee-script": "1.6.0" - } - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "sinon": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", - "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", - "dev": true, - "requires": { - "buster-format": "0.5.6" - }, - "dependencies": { - "buster-core": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", - "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", - "dev": true - }, - "buster-format": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", - "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", - "dev": true, - "requires": { - "buster-core": "0.6.4" - } - } - } - }, - "smoke-test-sharelatex": { - "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", - "requires": { - "mocha": "1.17.1" - }, - "dependencies": { - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "requires": { - "graceful-fs": "2.0.3", - "inherits": "2.0.3", - "minimatch": "0.2.14" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "mocha": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", - "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", - "requires": { - "commander": "2.0.0", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "diff": "1.0.7", - "glob": "3.2.3", - "growl": "1.7.0", - "jade": "0.26.3", - "mkdirp": "0.3.5" - } - } - } - }, - "split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" - }, - "sqlite3": { - "version": "3.1.13", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-3.1.13.tgz", - "integrity": "sha512-JxXKPJnkZ6NuHRojq+g2WXWBt3M1G9sjZaYiHEWSTGijDM3cwju/0T2XbWqMXFmPqDgw+iB7zKQvnns4bvzXlw==", - "requires": { - "nan": "2.7.0", - "node-pre-gyp": "0.6.38" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.3" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.8", - "bundled": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "caseless": { - "version": "0.12.0", - "bundled": true - }, - "co": { - "version": "4.6.0", - "bundled": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true - }, - "extsprintf": { - "version": "1.3.0", - "bundled": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "ini": { - "version": "1.3.4", - "bundled": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true - }, - "jsprim": { - "version": "1.4.1", - "bundled": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "mime-db": { - "version": "1.30.0", - "bundled": true - }, - "mime-types": { - "version": "2.1.17", - "bundled": true, - "requires": { - "mime-db": "1.30.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true - }, - "nan": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", - "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" - }, - "node-pre-gyp": { - "version": "0.6.38", - "bundled": true, - "requires": { - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.4.1", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true - }, - "qs": { - "version": "6.4.0", - "bundled": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true - } - } - }, - "readable-stream": { - "version": "2.3.3", - "bundled": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true - }, - "semver": { - "version": "5.4.1", - "bundled": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.1", - "bundled": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.3", - "bundled": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.3", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.3", - "bundled": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true - }, - "uuid": { - "version": "3.1.0", - "bundled": true - }, - "verror": { - "version": "1.10.0", - "bundled": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - } - } - }, - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - }, - "statsd-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", - "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha1-iD992rwWUUKyphQn8zUt7RldGj4=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - }, - "tar-fs": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", - "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", - "requires": { - "mkdirp": "0.5.1", - "pump": "1.0.3", - "tar-stream": "1.5.5" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - } - } - } - }, - "tar-stream": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", - "integrity": "sha1-XK2Ed59FyDsfJQjZawnYjHIYr1U=", - "requires": { - "bl": "1.2.1", - "end-of-stream": "1.4.1", - "readable-stream": "2.3.3", - "xtend": "4.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "1.0.0", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "string_decoder": "1.0.3", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - } - } - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "timekeeper": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", - "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "underscore": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" - }, - "util-deprecate": { - "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" - }, - "v8-profiler": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", - "integrity": "sha1-6DgcvrtbX9DKjSsJ9qAYGhWNs00=", - "requires": { - "nan": "2.8.0", - "node-pre-gyp": "0.6.39" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.4" - } - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.17" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" - }, - "node-pre-gyp": { - "version": "0.6.39", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "requires": { - "detect-libc": "1.0.3", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.5", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "2.2.1", - "tar-pack": "3.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "rc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz", - "integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=", - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "readable-stream": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz", - "integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.4", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } - }, - "wrappy": { - "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "wrench": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", - "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" } } } diff --git a/package.json b/package.json index fdb50307..e0e4fdaf 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", - "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile, skipping'", - "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile, skipping'", - "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests", + "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", + "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", + "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", "nodemon": "nodemon --config nodemon.json", "compile:smoke_tests": "[ -e test/smoke ] && coffee -o test/smoke/js -c test/smoke/coffee || echo 'No smoke tests to compile, skipping'" }, diff --git a/patch-texlive-dockerfile b/patch-texlive-dockerfile new file mode 100644 index 00000000..61cb7964 --- /dev/null +++ b/patch-texlive-dockerfile @@ -0,0 +1,3 @@ +FROM quay.io/sharelatex/texlive-full:2017.1 + +# RUN usermod -u 1001 tex diff --git a/synctex.profile b/synctex.profile new file mode 100644 index 00000000..577a9019 --- /dev/null +++ b/synctex.profile @@ -0,0 +1,34 @@ +include /etc/firejail/disable-common.inc +include /etc/firejail/disable-devel.inc +# include /etc/firejail/disable-mgmt.inc ## removed in 0.9.40 +# include /etc/firejail/disable-secret.inc ## removed in 0.9.40 + +read-only /bin +blacklist /boot +blacklist /dev +read-only /etc +blacklist /home # blacklisted for synctex +read-only /lib +read-only /lib64 +blacklist /media +blacklist /mnt +blacklist /opt +blacklist /root +read-only /run +blacklist /sbin +blacklist /selinux +blacklist /src +blacklist /sys +read-only /usr + +caps.drop all +noroot +nogroups +net none +private-tmp +private-dev +shell none +seccomp +nonewprivs + + From b330ee2d5b44e277cbc795b4fa4af242a2d8d3c1 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 29 Mar 2018 13:07:55 +0100 Subject: [PATCH 329/709] grep works with command updated build scripts acceptence tests break, files are written as root when user is node --- Makefile | 7 ++++--- app.coffee | 4 ++-- app/coffee/ResourceWriter.coffee | 6 ++++++ docker-compose-config.yml | 4 +++- docker-compose.ci.yml | 6 ++---- docker-compose.yml | 10 ++++++---- entrypoint.sh | 12 +++++++++--- install_deps.sh | 9 --------- package.json | 4 ++-- .../coffee/ExampleDocumentTests.coffee | 17 +++++++++++++---- 10 files changed, 47 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index 2a1196c2..2db377ab 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ BRANCH_NAME=$(BRANCH_NAME) \ PROJECT_NAME=$(PROJECT_NAME) \ + MOCHA_GREP=${MOCHA_GREP} \ docker-compose ${DOCKER_COMPOSE_FLAGS} clean: @@ -21,13 +22,13 @@ clean: test: test_unit test_acceptance test_unit: - @[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit -- ${MOCHA_ARGS} + @[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit test_acceptance: test_clean # clear the database before each acceptance test run - @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance -- ${MOCHA_ARGS} + @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance test_clean: - $(DOCKER_COMPOSE) down -t -v 0 + $(DOCKER_COMPOSE) down -v -t 0 build: docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . diff --git a/app.coffee b/app.coffee index a3f4ef17..11981a72 100644 --- a/app.coffee +++ b/app.coffee @@ -182,7 +182,7 @@ server = net.createServer (socket) -> logger.err err:err, "error with socket on load check" socket.destroy() - if STATE == "up" and settings.load_balancer_agent.report_load + if STATE == "up" and Settings.load_balancer_agent.report_load currentLoad = os.loadavg()[0] # staging clis's have 1 cpu core only @@ -206,7 +206,7 @@ server = net.createServer (socket) -> port = (Settings.internal?.clsi?.port or 3013) host = (Settings.internal?.clsi?.host or "localhost") -load_port = settings.internal.clsi.load_port or 3048 +load_port = Settings.internal.clsi.load_port or 3048 diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 0b6aef5b..90ada043 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -120,7 +120,13 @@ module.exports = ResourceWriter = logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources" callback() #try and continue compiling even if http resource can not be downloaded at this time else + process = require("process") + console.log "writing file out", path, process.getuid() fs.writeFile path, resource.content, callback + try + result = fs.lstatSync(path) + console.log "path stats", result + catch e checkPath: (basePath, resourcePath, callback) -> path = Path.normalize(Path.join(basePath, resourcePath)) diff --git a/docker-compose-config.yml b/docker-compose-config.yml index f2bb8631..b6deacff 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -12,6 +12,8 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock - ./compiles:/app/compiles + - ./cache:/app/cache + ci: environment: @@ -24,4 +26,4 @@ services: SQLITE_PATH: /app/compiles/db.sqlite volumes: - /var/run/docker.sock:/var/run/docker.sock:rw - - ./compiles:/app/compiles \ No newline at end of file + - ./cache:/app/cache diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 370e2a80..85512cf7 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -8,8 +8,7 @@ version: "2" services: test_unit: image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER - user: node - entrypoint: npm run test:unit:_run + command: npm run test:unit:_run test_acceptance: build: . @@ -23,8 +22,7 @@ services: depends_on: - redis - mongo - user: node - entrypoint: npm run test:acceptance:_run + command: npm run test:acceptance:_run redis: image: redis diff --git a/docker-compose.yml b/docker-compose.yml index 32cbf61e..5f53d691 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,9 @@ services: volumes: - .:/app working_dir: /app - user: node - entrypoint: npm run test:unit + environment: + MOCHA_GREP: ${MOCHA_GREP} + command: npm run test:unit test_acceptance: build: . @@ -25,11 +26,12 @@ services: environment: REDIS_HOST: redis MONGO_HOST: mongo - user: node + environment: + MOCHA_GREP: ${MOCHA_GREP} depends_on: - redis - mongo - entrypoint: npm run test:acceptance + command: npm run test:acceptance redis: image: redis diff --git a/entrypoint.sh b/entrypoint.sh index ee5ea47f..32776fa5 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,7 +1,10 @@ #!/bin/sh echo "Changing permissions of /var/run/docker.sock for sibling containers" - +ls -al /var/run/docker.sock +docker --version +cat /etc/passwd +usermod -aG docker node chown root:docker /var/run/docker.sock mkdir -p /app/cache @@ -10,7 +13,10 @@ chown -R node:node /app/cache mkdir -p /app/compiles chown -R node:node /app/compiles +chown -R node:node /app/bin/synctex +mkdir -p /app/test/acceptance/fixtures/tmp/ +chown -R node:node /app + + ./bin/install_texlive_gce.sh -echo "HELOOOo" -echo "$@" exec runuser -u node "$@" \ No newline at end of file diff --git a/install_deps.sh b/install_deps.sh index 4b06f22d..49bdc5c9 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -2,12 +2,3 @@ wget -qO- https://get.docker.com/ | sh apt-get install poppler-utils vim ghostscript --yes npm rebuild -usermod -aG docker node - -mkdir -p /app/cache -chown -R node:node /app/cache - -mkdir -p /app/compiles -chown -R node:node /app/compiles - -chown -R node:node /app/bin/synctex diff --git a/package.json b/package.json index e0e4fdaf..fe2bd260 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", - "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@", + "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", - "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@", + "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index ec569796..d4bd19f5 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -4,15 +4,21 @@ require("chai").should() fs = require "fs" ChildProcess = require "child_process" ClsiApp = require "./helpers/ClsiApp" - -fixturePath = (path) -> __dirname + "/../fixtures/" + path - +logger = require("logger-sharelatex") +Path = require("path") +fixturePath = (path) -> Path.normalize(__dirname + "/../fixtures/" + path) +process = require "process" +console.log process.pid, process.ppid, process.getuid(),process.getgroups(), "PID" try + console.log "creating tmp directory", fixturePath("tmp") fs.mkdirSync(fixturePath("tmp")) -catch e +catch err + console.log err, fixturePath("tmp"), "unable to create fixture tmp path" convertToPng = (pdfPath, pngPath, callback = (error) ->) -> command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" + console.log "COMMAND" + console.log command convert = ChildProcess.exec command stdout = "" convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() @@ -58,6 +64,8 @@ compareMultiplePages = (project_id, callback = (error) ->) -> compareNext 0, callback comparePdf = (project_id, example_dir, callback = (error) ->) -> + console.log "CONVERT" + console.log "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png" convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => throw error if error? convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => @@ -76,6 +84,7 @@ comparePdf = (project_id, example_dir, callback = (error) ->) -> downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) -> writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf")) request.get(url).pipe(writeStream) + console.log("writing file out", fixturePath("tmp/#{project_id}.pdf")) writeStream.on "close", () => checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) => throw error if error? From ca23cd42ada5803d6237506572f078e00d354c77 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 9 Apr 2018 11:06:35 +0100 Subject: [PATCH 330/709] update package.json scripts --- docker-compose.yml | 1 - package.json | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5f53d691..5a25a011 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,6 @@ services: environment: REDIS_HOST: redis MONGO_HOST: mongo - environment: MOCHA_GREP: ${MOCHA_GREP} depends_on: - redis diff --git a/package.json b/package.json index fe2bd260..49835cc7 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", - "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", - "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'", - "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'", + "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", + "compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee", + "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", "nodemon": "nodemon --config nodemon.json", "compile:smoke_tests": "[ -e test/smoke ] && coffee -o test/smoke/js -c test/smoke/coffee || echo 'No smoke tests to compile, skipping'" From 1d38dd3a9238dc5ad4fbabf03b8117e0ce7dac4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez=20Capel?= <alberto.fernandez-capel@overleaf.com> Date: Tue, 1 May 2018 09:25:37 +0100 Subject: [PATCH 331/709] Make travis read the node version from the .nvmrc file See https://docs.travis-ci.com/user/languages/javascript-with-nodejs/#Specifying-Node.js-versions-using-.nvmrc --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29f5884d..b6fb0e91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,5 @@ language: node_js -node_js: - - "0.10" - before_install: - npm install -g grunt-cli From 98a4e60eb7456c859a9075181ee2c5ed2e01d587 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 24 May 2018 19:03:57 +0100 Subject: [PATCH 332/709] update to 1.1.3 build scripts --- .dockerignore | 6 +++ .nvmrc | 2 +- Dockerfile | 15 +++--- Makefile | 11 +++-- bin/synctex | Bin 90472 -> 92840 bytes docker-compose.ci.yml | 10 ++-- docker-compose.yml | 9 ++-- nodemon.json | 2 + package.json | 104 +++++++++++++++++++++--------------------- 9 files changed, 88 insertions(+), 71 deletions(-) diff --git a/.dockerignore b/.dockerignore index a9983535..386f26df 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,9 @@ node_modules/* +gitrev +.git +.gitignore +.npm +.nvmrc +nodemon.json app.js **/js/* diff --git a/.nvmrc b/.nvmrc index 5917993c..bbf0c5a5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.13.0 +6.14.1 diff --git a/Dockerfile b/Dockerfile index ed1f2c6e..1ccc689d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,23 @@ -FROM node:6.13.0 as app - -COPY ./ /app +FROM node:6.14.1 as app WORKDIR /app -RUN npm install +#wildcard as some files may not be in all repos +COPY package*.json npm-shrink*.json /app/ + +RUN npm install --quiet + +COPY . /app RUN npm run compile:all -FROM node:6.13.0 +FROM node:6.14.1 COPY --from=app /app /app WORKDIR /app -RUN ./install_deps.sh +RUN chmod 0755 ./install_deps.sh && ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] CMD ["node","app.js"] diff --git a/Makefile b/Makefile index 2db377ab..63ff8c02 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.0 +# Version: 1.1.3 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -13,6 +13,7 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ MOCHA_GREP=${MOCHA_GREP} \ docker-compose ${DOCKER_COMPOSE_FLAGS} + clean: rm -f app.js rm -rf app/js @@ -24,16 +25,18 @@ test: test_unit test_acceptance test_unit: @[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit -test_acceptance: test_clean # clear the database before each acceptance test run +test_acceptance: test_clean test_acceptance_pre_run # clear the database before each acceptance test run @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance test_clean: $(DOCKER_COMPOSE) down -v -t 0 +test_acceptance_pre_run: + @[ ! -f test/acceptance/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run build: - docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . + docker build --pull --tag gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . publish: - docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + docker push gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/bin/synctex b/bin/synctex index 99044b97a0259b30afbf0bfb34365e4f1270479e..e793e248bb52d48f28a105fb6dd2ea9ead1659e1 100755 GIT binary patch literal 92840 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 literal 90472 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:nwe6m+2D>* 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 Ww?EQ8J?~lm(LUq<N}>K2<Nq%(A5^6P diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 85512cf7..0e14b0f0 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,27 +1,28 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.0 +# Version: 1.1.3 version: "2" services: test_unit: - image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + image: gcr.io/csh-gcdm-test/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER command: npm run test:unit:_run test_acceptance: build: . - image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + image: gcr.io/csh-gcdm-test/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER extends: file: docker-compose-config.yml service: ci environment: REDIS_HOST: redis MONGO_HOST: mongo + POSTGRES_HOST: postgres depends_on: - - redis - mongo + - redis command: npm run test:acceptance:_run redis: @@ -29,3 +30,4 @@ services: mongo: image: mongo:3.4 + diff --git a/docker-compose.yml b/docker-compose.yml index 5a25a011..22837461 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,13 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.0 +# Version: 1.1.3 version: "2" services: test_unit: - image: node:6.13.0 + build: . volumes: - .:/app working_dir: /app @@ -26,14 +26,15 @@ services: environment: REDIS_HOST: redis MONGO_HOST: mongo + POSTGRES_HOST: postgres MOCHA_GREP: ${MOCHA_GREP} depends_on: - - redis - mongo + - redis command: npm run test:acceptance - redis: image: redis mongo: image: mongo:3.4 + diff --git a/nodemon.json b/nodemon.json index 815f3922..98db38d7 100644 --- a/nodemon.json +++ b/nodemon.json @@ -8,10 +8,12 @@ "execMap": { "js": "npm run start" }, + "watch": [ "app/coffee/", "app.coffee", "config/" ], "ext": "coffee" + } diff --git a/package.json b/package.json index 49835cc7..c9257080 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,63 @@ { - "name": "node-clsi", - "description": "A Node.js implementation of the CLSI LaTeX web-API", - "version": "0.1.4", + "name": "node-clsi", + "description": "A Node.js implementation of the CLSI LaTeX web-API", + "version": "0.1.4", "repository": { - "type": "git", + "type": "git", "url": "https://github.com/sharelatex/clsi-sharelatex.git" - }, + }, "scripts": { - "compile:app": "coffee $COFFEE_OPTIONS -o app/js -c app/coffee && coffee $COFFEE_OPTIONS -c app.coffee", - "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", - "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", - "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --recursive --exit --reporter spec $@ test/unit/js", - "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", - "compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee", - "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", - "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", - "nodemon": "nodemon --config nodemon.json", + "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", + "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", + "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", + "compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee", + "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", + "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", + "nodemon": "nodemon --config nodemon.json", "compile:smoke_tests": "[ -e test/smoke ] && coffee -o test/smoke/js -c test/smoke/coffee || echo 'No smoke tests to compile, skipping'" - }, - "author": "James Allen <james@sharelatex.com>", + }, + "author": "James Allen <james@sharelatex.com>", "dependencies": { - "async": "0.2.9", - "body-parser": "^1.2.0", - "dockerode": "^2.5.3", - "express": "^4.2.0", - "fs-extra": "^0.16.3", - "grunt-mkdir": "^1.0.0", - "heapdump": "^0.3.5", - "lockfile": "^1.0.3", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", - "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", - "mkdirp": "0.3.5", - "mysql": "2.6.2", - "request": "^2.21.0", - "sequelize": "^2.1.3", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "^3.1.13", - "underscore": "^1.8.2", - "v8-profiler": "^5.2.4", + "async": "0.2.9", + "body-parser": "^1.2.0", + "dockerode": "^2.5.3", + "express": "^4.2.0", + "fs-extra": "^0.16.3", + "grunt-mkdir": "^1.0.0", + "heapdump": "^0.3.5", + "lockfile": "^1.0.3", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", + "lynx": "0.0.11", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", + "mkdirp": "0.3.5", + "mysql": "2.6.2", + "request": "^2.21.0", + "sequelize": "^2.1.3", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", + "sqlite3": "^3.1.13", + "underscore": "^1.8.2", + "v8-profiler": "^5.2.4", "wrench": "~1.5.4" - }, + }, "devDependencies": { - "mocha": "^4.0.1", - "coffee-script": "1.6.0", - "chai": "~1.8.1", - "sinon": "~1.7.3", - "grunt": "~0.4.2", - "grunt-contrib-coffee": "~0.7.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-shell": "~0.6.1", - "grunt-mocha-test": "~0.8.1", - "sandboxed-module": "~0.3.0", - "timekeeper": "0.0.4", - "grunt-execute": "^0.1.5", - "bunyan": "^0.22.1", + "mocha": "^4.0.1", + "coffee-script": "1.6.0", + "chai": "~1.8.1", + "sinon": "~1.7.3", + "grunt": "~0.4.2", + "grunt-contrib-coffee": "~0.7.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-shell": "~0.6.1", + "grunt-mocha-test": "~0.8.1", + "sandboxed-module": "~0.3.0", + "timekeeper": "0.0.4", + "grunt-execute": "^0.1.5", + "bunyan": "^0.22.1", "grunt-bunyan": "^0.5.0" } } From 1814f1c997c3c0fe4f675c80676b9f070518bfb3 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 24 May 2018 21:59:02 +0100 Subject: [PATCH 333/709] added --exit to unit tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9257080..a0ea4f03 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", "compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee", "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", From e544ad9a238f847258a21e5bcc196ffe7ec1cace Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 25 May 2018 11:51:34 +0100 Subject: [PATCH 334/709] set user to tex for tests run on ci box --- docker-compose-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-config.yml b/docker-compose-config.yml index b6deacff..ce0c502d 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -18,7 +18,7 @@ services: ci: environment: TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 - TEXLIVE_IMAGE_USER: root + TEXLIVE_IMAGE_USER: tex SHARELATEX_CONFIG: /app/config/settings.defaults.coffee DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles From da814b0e3a7de59c46363957ec8a626eb44ebe68 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 25 May 2018 12:01:16 +0100 Subject: [PATCH 335/709] log settings on startup --- config/settings.defaults.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index de853f59..ab6e7e6d 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -46,3 +46,4 @@ if process.env["DOCKER_RUNNER"] module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"] +console.log module.exports \ No newline at end of file From 95486151697ba5f2d2002d2fd41a6acce5c5349d Mon Sep 17 00:00:00 2001 From: henry oswald <henry.oswald@gmail.com> Date: Fri, 25 May 2018 12:43:12 +0000 Subject: [PATCH 336/709] all but the sync tests should pass --- config/settings.defaults.coffee | 3 ++- docker-compose-config.yml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index ab6e7e6d..2da68349 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -46,4 +46,5 @@ if process.env["DOCKER_RUNNER"] module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"] -console.log module.exports \ No newline at end of file +console.log "configggggg" +console.log module.exports diff --git a/docker-compose-config.yml b/docker-compose-config.yml index ce0c502d..3b780604 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -4,7 +4,7 @@ services: dev: environment: TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 - TEXLIVE_IMAGE_USER: root + TEXLIVE_IMAGE_USER: "tex" SHARELATEX_CONFIG: /app/config/settings.defaults.coffee DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles @@ -18,7 +18,7 @@ services: ci: environment: TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 - TEXLIVE_IMAGE_USER: tex + TEXLIVE_IMAGE_USER: "tex" SHARELATEX_CONFIG: /app/config/settings.defaults.coffee DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles @@ -27,3 +27,4 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock:rw - ./cache:/app/cache + - ./compiles:/app/compiles From 0b3af7d75988e81010f5a82499d6bf4ecaaa91eb Mon Sep 17 00:00:00 2001 From: henry oswald <henry.oswald@gmail.com> Date: Fri, 25 May 2018 13:45:07 +0000 Subject: [PATCH 337/709] change synctex binary and added it to mounted volumes in docker config --- bin/synctex | Bin 92840 -> 92776 bytes docker-compose-config.yml | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/synctex b/bin/synctex index e793e248bb52d48f28a105fb6dd2ea9ead1659e1..89b8cc698827b5e02a1b6053f32098766304b739 100755 GIT binary patch literal 92776 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 literal 92840 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 diff --git a/docker-compose-config.yml b/docker-compose-config.yml index 3b780604..c8b7dcc2 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -13,6 +13,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ./compiles:/app/compiles - ./cache:/app/cache + - ./bin/synctex:/app/bin/synctex ci: @@ -26,5 +27,6 @@ services: SQLITE_PATH: /app/compiles/db.sqlite volumes: - /var/run/docker.sock:/var/run/docker.sock:rw - - ./cache:/app/cache - ./compiles:/app/compiles + - ./cache:/app/cache + - ./bin/synctex:/app/bin/synctex From 38e91ab3e438716a494eb61b9636bf1c2151333e Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 25 May 2018 15:30:26 +0100 Subject: [PATCH 338/709] bumped timeout to 30 seconds --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0ea4f03..49a259a9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", From 85aec72206be30020e92f38ac046f16923258daf Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 12 Jun 2018 10:15:17 +0100 Subject: [PATCH 339/709] Use metadata to determine Google Cloud project dynamically. Fixes: #601 --- bin/install_texlive_gce.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/install_texlive_gce.sh b/bin/install_texlive_gce.sh index 85ab7096..4f98fed7 100755 --- a/bin/install_texlive_gce.sh +++ b/bin/install_texlive_gce.sh @@ -1,13 +1,19 @@ #!/bin/sh METADATA=http://metadata.google.internal./computeMetadata/v1 SVC_ACCT=$METADATA/instance/service-accounts/default +PROJECT_URL=$METADATA/project/project-id ACCESS_TOKEN=$(curl -s -H 'Metadata-Flavor: Google' $SVC_ACCT/token | cut -d'"' -f 4) -if [ -z "$ACCESS_TOKEN" ]; then - echo "No acccess token to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." +#if [ -z "$ACCESS_TOKEN" ]; then +# echo "No acccess token to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." +# exit 0 +#fi +PROJECT=$(curl -s -H 'Metadata-Flavor: Google' $PROJECT_URL) +if [ -z "$PROJECT" ]; then + echo "No project name to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." exit 0 fi docker login -u '_token' -p $ACCESS_TOKEN https://gcr.io -docker pull --all-tags gcr.io/henry-terraform-admin/texlive-full #TODO NEED TO MAKE THIS AN ENV VAR +docker pull --all-tags gcr.io/$PROJECT/texlive-full cp /app/bin/synctex /app/bin/synctex-mount/synctex echo "Finished downloading texlive-full images" From e6532b5681086db89c3cb54e55f264a9301a2238 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 12 Jun 2018 10:22:30 +0100 Subject: [PATCH 340/709] Update build scripts from 1.1.3 to 1.1.6 --- Jenkinsfile | 9 ++++++++- Makefile | 11 ++++++++--- docker-compose.ci.yml | 9 +++++---- docker-compose.yml | 4 +++- package.json | 4 ++-- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index bc9ba014..eddce50d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -29,7 +29,13 @@ pipeline { stage('Package and publish build') { steps { - sh 'make publish' + + withCredentials([file(credentialsId: 'csh-gcdm-test', variable: 'DOCKER_REPO_KEY_PATH')]) { + sh 'docker login -u _json_key --password-stdin https://csh-gcdm-test < ${DOCKER_REPO_KEY_PATH}' + } + sh 'DOCKER_REPO=csh-gcdm-test make publish' + sh 'docker logout https://csh-gcdm-test' + } } @@ -47,6 +53,7 @@ pipeline { post { always { sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_clean' + sh 'make clean' } failure { diff --git a/Makefile b/Makefile index 63ff8c02..ae7cf16e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.3 +# Version: 1.1.6 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -15,6 +15,8 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ clean: + docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + docker rmi csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) rm -f app.js rm -rf app/js rm -rf test/unit/js @@ -34,9 +36,12 @@ test_clean: test_acceptance_pre_run: @[ ! -f test/acceptance/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run build: - docker build --pull --tag gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . + docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + . publish: - docker push gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + + docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 0e14b0f0..651b4546 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,25 +1,27 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.3 +# Version: 1.1.6 version: "2" services: test_unit: - image: gcr.io/csh-gcdm-test/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER command: npm run test:unit:_run test_acceptance: build: . - image: gcr.io/csh-gcdm-test/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER extends: file: docker-compose-config.yml service: ci environment: + ELASTIC_SEARCH_DSN: es:9200 REDIS_HOST: redis MONGO_HOST: mongo POSTGRES_HOST: postgres + MOCHA_GREP: ${MOCHA_GREP} depends_on: - mongo - redis @@ -30,4 +32,3 @@ services: mongo: image: mongo:3.4 - diff --git a/docker-compose.yml b/docker-compose.yml index 22837461..a0b42619 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.3 +# Version: 1.1.6 version: "2" @@ -24,6 +24,7 @@ services: file: docker-compose-config.yml service: dev environment: + ELASTIC_SEARCH_DSN: es:9200 REDIS_HOST: redis MONGO_HOST: mongo POSTGRES_HOST: postgres @@ -32,6 +33,7 @@ services: - mongo - redis command: npm run test:acceptance + redis: image: redis diff --git a/package.json b/package.json index 49a259a9..8bcdba29 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", + "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", "compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee", "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", From da216c52e961a74c31b5111d26384baf51ecf75d Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 12 Jun 2018 11:17:26 +0100 Subject: [PATCH 341/709] Accidently left warning message commented out :( --- bin/install_texlive_gce.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/install_texlive_gce.sh b/bin/install_texlive_gce.sh index 4f98fed7..ee6efba4 100755 --- a/bin/install_texlive_gce.sh +++ b/bin/install_texlive_gce.sh @@ -3,10 +3,10 @@ METADATA=http://metadata.google.internal./computeMetadata/v1 SVC_ACCT=$METADATA/instance/service-accounts/default PROJECT_URL=$METADATA/project/project-id ACCESS_TOKEN=$(curl -s -H 'Metadata-Flavor: Google' $SVC_ACCT/token | cut -d'"' -f 4) -#if [ -z "$ACCESS_TOKEN" ]; then -# echo "No acccess token to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." -# exit 0 -#fi +if [ -z "$ACCESS_TOKEN" ]; then + echo "No acccess token to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." + exit 0 +fi PROJECT=$(curl -s -H 'Metadata-Flavor: Google' $PROJECT_URL) if [ -z "$PROJECT" ]; then echo "No project name to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." From 4ca8027cb8f7c0e4beda1341c2d49f5de9fe8219 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 12 Jun 2018 15:04:14 +0100 Subject: [PATCH 342/709] Increase acceptance test timeout. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bcdba29..92265ebf 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", From 0a70985ba55dd8afd1933212d5b5bad7850e2486 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 12 Jun 2018 15:26:10 +0100 Subject: [PATCH 343/709] Specify repo correctly --- Jenkinsfile | 8 ++++---- Makefile | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index eddce50d..b88e3e44 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -30,11 +30,11 @@ pipeline { stage('Package and publish build') { steps { - withCredentials([file(credentialsId: 'csh-gcdm-test', variable: 'DOCKER_REPO_KEY_PATH')]) { - sh 'docker login -u _json_key --password-stdin https://csh-gcdm-test < ${DOCKER_REPO_KEY_PATH}' + withCredentials([file(credentialsId: 'gcr.io_csh-gcdm-test', variable: 'DOCKER_REPO_KEY_PATH')]) { + sh 'docker login -u _json_key --password-stdin https://gcr.io/csh-gcdm-test < ${DOCKER_REPO_KEY_PATH}' } - sh 'DOCKER_REPO=csh-gcdm-test make publish' - sh 'docker logout https://csh-gcdm-test' + sh 'DOCKER_REPO=gcr.io/csh-gcdm-test make publish' + sh 'docker logout https://gcr.io/csh-gcdm-test' } } diff --git a/Makefile b/Makefile index ae7cf16e..a1cefa4c 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ clean: docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - docker rmi csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + docker rmi gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) rm -f app.js rm -rf app/js rm -rf test/unit/js @@ -37,7 +37,7 @@ test_acceptance_pre_run: @[ ! -f test/acceptance/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ - --tag csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ . publish: From 926667f365ed988d12f89bac6aaada6eda4f5def Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 12 Jun 2018 17:44:13 +0100 Subject: [PATCH 344/709] update build scripts so smoke tests are compiled --- Makefile | 2 +- docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- package.json | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index a1cefa4c..ca126e8e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.6 +# Version: 1.1.7 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 651b4546..9f3d54d1 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.6 +# Version: 1.1.7 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index a0b42619..4c1cfc73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.6 +# Version: 1.1.7 version: "2" diff --git a/package.json b/package.json index 92265ebf..54625fd2 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", "compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee", "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", - "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests", + "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests", "nodemon": "nodemon --config nodemon.json", - "compile:smoke_tests": "[ -e test/smoke ] && coffee -o test/smoke/js -c test/smoke/coffee || echo 'No smoke tests to compile, skipping'" + "compile:smoke_tests": "[ ! -e test/smoke/coffee] && echo 'No smoke tests to compile' || coffee -o test/smoke/js -c test/smoke/coffee" }, "author": "James Allen <james@sharelatex.com>", "dependencies": { From b30890ef9948030c44e36e16cbc3fb8c318f9131 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 12 Jun 2018 17:48:23 +0100 Subject: [PATCH 345/709] remove the compile npm command, it isn't needed --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 54625fd2..79f9c806 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ }, "scripts": { "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", - "compile": "npm run compile:app && npm run compile:test:acceptance && npm run compile:test:smoke", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", From b3033c16860437892241673cb8a62e35cbe5e31f Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Wed, 13 Jun 2018 15:47:45 +0100 Subject: [PATCH 346/709] Add csh-staging to repos --- Jenkinsfile | 6 ++++++ Makefile | 2 ++ 2 files changed, 8 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index b88e3e44..efe84483 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -36,6 +36,12 @@ pipeline { sh 'DOCKER_REPO=gcr.io/csh-gcdm-test make publish' sh 'docker logout https://gcr.io/csh-gcdm-test' + withCredentials([file(credentialsId: 'gcr.io_csh-staging', variable: 'DOCKER_REPO_KEY_PATH')]) { + sh 'docker login -u _json_key --password-stdin https://gcr.io/csh-staging < ${DOCKER_REPO_KEY_PATH}' + } + sh 'DOCKER_REPO=gcr.io/csh-staging make publish' + sh 'docker logout https://gcr.io/csh-staging' + } } diff --git a/Makefile b/Makefile index ca126e8e..b7c96a2b 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ clean: docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) docker rmi gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + docker rmi gcr.io/csh-staging/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) rm -f app.js rm -rf app/js rm -rf test/unit/js @@ -38,6 +39,7 @@ test_acceptance_pre_run: build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ --tag gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag gcr.io/csh-staging/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ . publish: From 82b996b145196711e439d7d7045f53498c1afa1a Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Mon, 25 Jun 2018 14:06:18 +0100 Subject: [PATCH 347/709] increase timeout on wordcount --- app/coffee/CompileManager.coffee | 2 +- test/unit/coffee/CompileManagerTests.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 167a80e1..b0573d1a 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -281,7 +281,7 @@ module.exports = CompileManager = file_path = "$COMPILE_DIR/" + file_name command = [ "texcount", '-nocol', '-inc', file_path, "-out=" + file_path + ".wc"] directory = getCompileDir(project_id, user_id) - timeout = 10 * 1000 + timeout = 60 * 1000 # increased to allow for large projects compileName = getCompileName(project_id, user_id) CommandRunner.run compileName, command, directory, image, timeout, {}, (error) -> diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 341ce2d0..14ddb2e1 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -298,7 +298,7 @@ describe "CompileManager", -> @callback = sinon.stub() @project_id = "project-id-123" - @timeout = 10 * 1000 + @timeout = 60 * 1000 @file_name = "main.tex" @Settings.path.compilesDir = "/local/compile/directory" @image = "example.com/image" From dd93d37460a759571a00c838a85e52b028944bd3 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 26 Jun 2018 12:43:47 +0100 Subject: [PATCH 348/709] added seccomp --- config/settings.defaults.coffee | 1 + seccomp/clsi-profile.json | 792 ++++++++++++++++++++++++++++++++ 2 files changed, 793 insertions(+) create mode 100644 seccomp/clsi-profile.json diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 2da68349..ced53a91 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -40,6 +40,7 @@ if process.env["DOCKER_RUNNER"] user: process.env["TEXLIVE_IMAGE_USER"] or "tex" expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 checkProjectsIntervalMs: 10 * 60 * 1000 + seccomp_profile: JSON.stringify(JSON.parse(require("fs").readFileSync(Path.resolve(__dirname + "/../seccomp/clsi-profile.json")))) module.exports.path.synctexBaseDir = -> "/compile" diff --git a/seccomp/clsi-profile.json b/seccomp/clsi-profile.json new file mode 100644 index 00000000..cab7a445 --- /dev/null +++ b/seccomp/clsi-profile.json @@ -0,0 +1,792 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32" + ], + "syscalls": [ + { + "name": "access", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "arch_prctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "brk", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chmod", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clone", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2080505856, + "valueTwo": 0, + "op": "SCMP_CMP_MASKED_EQ" + } + ] + }, + { + "name": "close", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "copy_file_range", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "creat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup3", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "execve", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "execveat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "exit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "exit_group", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "faccessat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fadvise64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fadvise64_64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fallocate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchmod", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchmodat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fcntl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fcntl64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fdatasync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fork", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatfs64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fsync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ftruncate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ftruncate64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "futex", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "futimesat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getcpu", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getcwd", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getdents", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getdents64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getegid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getegid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "geteuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "geteuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgroups", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgroups32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpgrp", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getppid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpriority", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getrlimit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "get_robust_list", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getrusage", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getsid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "gettid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ioctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "kill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "_llseek", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lseek", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lstat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lstat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "madvise", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mkdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mkdirat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mmap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mmap2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mprotect", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mremap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "munmap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "newfstatat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "open", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "openat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pause", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pipe", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pipe2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "prctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pread64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "preadv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "prlimit64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pwrite64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pwritev", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "read", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readlinkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rename", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "renameat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "renameat2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "restart_syscall", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rmdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigaction", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigpending", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigprocmask", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigqueueinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigreturn", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigsuspend", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigtimedwait", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_tgsigqueueinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getaffinity", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getparam", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_get_priority_max", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_get_priority_min", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getscheduler", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_rr_get_interval", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_yield", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendfile", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendfile64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgroups", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgroups32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_robust_list", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_tid_address", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sigaltstack", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "stat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "stat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "statfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "statfs64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sync_file_range", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "syncfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sysinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "tgkill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "times", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "tkill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "truncate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "truncate64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "umask", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "uname", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "unlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "unlinkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utimensat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utimes", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "vfork", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "vhangup", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "wait4", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "waitid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "write", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "writev", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pread", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "capget", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "capset", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchown", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "gettimeofday", + "action": "SCMP_ACT_ALLOW", + "args": [] + } + ] +} \ No newline at end of file From 911e1d58f7f863168b7e9d7a4b1d23b939a59d39 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 26 Jun 2018 14:44:03 +0100 Subject: [PATCH 349/709] put seccomp_profile_path into variable and try catch --- config/settings.defaults.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index ced53a91..901637cd 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -40,7 +40,12 @@ if process.env["DOCKER_RUNNER"] user: process.env["TEXLIVE_IMAGE_USER"] or "tex" expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 checkProjectsIntervalMs: 10 * 60 * 1000 - seccomp_profile: JSON.stringify(JSON.parse(require("fs").readFileSync(Path.resolve(__dirname + "/../seccomp/clsi-profile.json")))) + + try + seccomp_profile_path = Path.resolve(__dirname + "/../seccomp/clsi-profile.json") + module.exports.clsi.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path))) + catch + console.log "could not load seccom profile from #{seccomp_profile_path}" module.exports.path.synctexBaseDir = -> "/compile" From 364c8097c88e4c830400a2ef3deafec2d0a310b5 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 26 Jun 2018 15:04:56 +0100 Subject: [PATCH 350/709] add error catch to settings.defaults --- config/settings.defaults.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 901637cd..a5b6f1bd 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -44,8 +44,8 @@ if process.env["DOCKER_RUNNER"] try seccomp_profile_path = Path.resolve(__dirname + "/../seccomp/clsi-profile.json") module.exports.clsi.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path))) - catch - console.log "could not load seccom profile from #{seccomp_profile_path}" + catch error + console.log error, "could not load seccom profile from #{seccomp_profile_path}" module.exports.path.synctexBaseDir = -> "/compile" From 4bfc02ef3b16dcf0f6edf2ffa79ba60c804652ab Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 26 Jun 2018 15:38:30 +0100 Subject: [PATCH 351/709] fix seccomp key --- config/settings.defaults.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index a5b6f1bd..da4aa82c 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -43,7 +43,7 @@ if process.env["DOCKER_RUNNER"] try seccomp_profile_path = Path.resolve(__dirname + "/../seccomp/clsi-profile.json") - module.exports.clsi.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path))) + module.exports.clsi.docker.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path))) catch error console.log error, "could not load seccom profile from #{seccomp_profile_path}" From ec85957ae490b7439ec82a743c5d9e200e4dbdd6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 28 Jun 2018 16:04:34 +0100 Subject: [PATCH 352/709] add load balance http endpoints to shut box down --- app.coffee | 30 +++++++++++++++++++++++------- config/settings.defaults.coffee | 9 ++++++--- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app.coffee b/app.coffee index 11981a72..cf01b479 100644 --- a/app.coffee +++ b/app.coffee @@ -174,7 +174,11 @@ os = require "os" STATE = "up" -server = net.createServer (socket) -> +process.on "SIGHUP", -> + console.log "got SIGHUP event" + STATE = "down" + +loadTcpServer = net.createServer (socket) -> socket.on "error", (err)-> if err.code == "ECONNRESET" # this always comes up, we don't know why @@ -182,7 +186,7 @@ server = net.createServer (socket) -> logger.err err:err, "error with socket on load check" socket.destroy() - if STATE == "up" and Settings.load_balancer_agent.report_load + if STATE == "up" and Settings.internal.load_balancer_agent.report_load currentLoad = os.loadavg()[0] # staging clis's have 1 cpu core only @@ -201,25 +205,37 @@ server = net.createServer (socket) -> socket.write("#{STATE}\n", "ASCII") socket.end() +loadHttpServer = express() +loadHttpServer.post "/state/up", (req, res, next) -> + STATE = "up" + logger.info "getting message to set server to down" + res.sendStatus 204 +loadHttpServer.post "/state/down", (req, res, next) -> + STATE = "down" + logger.info "getting message to set server to down" + res.sendStatus 204 port = (Settings.internal?.clsi?.port or 3013) host = (Settings.internal?.clsi?.host or "localhost") -load_port = Settings.internal.clsi.load_port or 3048 - +load_tcp_port = Settings.internal.load_balancer_agent.load_port +load_http_port = Settings.internal.load_balancer_agent.local_port if !module.parent # Called directly app.listen port, host, (error) -> logger.info "CLSI starting up, listening on #{host}:#{port}" - server.listen load_port, host, (error) -> + loadTcpServer.listen load_tcp_port, host, (error) -> throw error if error? - logger.info "Load agent listening on load port #{load_port}" -module.exports = app + logger.info "Load tcp agent listening on load port #{load_tcp_port}" + loadHttpServer.listen load_http_port, host, (error) -> + throw error if error? + logger.info "Load http agent listening on load port #{load_http_port}" +module.exports = app setInterval () -> ProjectPersistenceManager.clearExpiredProjects() diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index da4aa82c..a64941f8 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -20,7 +20,11 @@ module.exports = clsi: port: 3013 host: process.env["LISTEN_ADDRESS"] or "localhost" - + + load_balancer_agent: + report_load:true + load_port: 3048 + local_port: 3049 apis: clsi: url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" @@ -29,6 +33,7 @@ module.exports = project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 + if process.env["DOCKER_RUNNER"] module.exports.clsi = dockerRunner: process.env["DOCKER_RUNNER"] == "true" @@ -52,5 +57,3 @@ if process.env["DOCKER_RUNNER"] module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"] -console.log "configggggg" -console.log module.exports From 6464aefdb4dd4d7ba358f9791a550ca59d53a009 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 3 Jul 2018 16:41:34 +0100 Subject: [PATCH 353/709] added filestoreDomainOveride --- app/coffee/UrlFetcher.coffee | 6 +++++ config/settings.defaults.coffee | 1 + test/unit/coffee/UrlFetcherTests.coffee | 35 +++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.coffee index 201306c0..58645f00 100644 --- a/app/coffee/UrlFetcher.coffee +++ b/app/coffee/UrlFetcher.coffee @@ -1,6 +1,8 @@ request = require("request").defaults(jar: false) fs = require("fs") logger = require "logger-sharelatex" +settings = require("settings-sharelatex") +URL = require('url'); oneMinute = 60 * 1000 @@ -11,6 +13,9 @@ module.exports = UrlFetcher = _callback(error) _callback = () -> + if settings.filestoreDomainOveride? + p = URL.parse(url).path + url = "#{settings.filestoreDomainOveride}#{p}" timeoutHandler = setTimeout () -> timeoutHandler = null logger.error url:url, filePath: filePath, "Timed out downloading file to cache" @@ -31,6 +36,7 @@ module.exports = UrlFetcher = logger.log url:url, filePath: filePath, "finished downloading file into cache" urlStream.on "response", (res) -> + console.log if res.statusCode >= 200 and res.statusCode < 300 fileStream = fs.createWriteStream(filePath) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index a64941f8..081ded81 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -32,6 +32,7 @@ module.exports = smokeTest: process.env["SMOKE_TEST"] or false project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 + filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] if process.env["DOCKER_RUNNER"] diff --git a/test/unit/coffee/UrlFetcherTests.coffee b/test/unit/coffee/UrlFetcherTests.coffee index 4bd161bc..e91720e5 100644 --- a/test/unit/coffee/UrlFetcherTests.coffee +++ b/test/unit/coffee/UrlFetcherTests.coffee @@ -7,16 +7,47 @@ EventEmitter = require("events").EventEmitter describe "UrlFetcher", -> beforeEach -> @callback = sinon.stub() - @url = "www.example.com/file" + @url = "https://www.example.com/file/here?query=string" @UrlFetcher = SandboxedModule.require modulePath, requires: request: defaults: @defaults = sinon.stub().returns(@request = {}) fs: @fs = {} "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + "settings-sharelatex": @settings = {} it "should turn off the cookie jar in request", -> @defaults.calledWith(jar: false) .should.equal true - + + describe "rewrite url domain if filestoreDomainOveride is set", -> + beforeEach -> + @path = "/path/to/file/on/disk" + @request.get = sinon.stub().returns(@urlStream = new EventEmitter) + @urlStream.pipe = sinon.stub() + @urlStream.pause = sinon.stub() + @urlStream.resume = sinon.stub() + @fs.createWriteStream = sinon.stub().returns(@fileStream = new EventEmitter) + @fs.unlink = (file, callback) -> callback() + + it "should use the normal domain when override not set", (done)-> + @UrlFetcher.pipeUrlToFile @url, @path, => + @request.get.args[0][0].url.should.equal @url + done() + @res = statusCode: 200 + @urlStream.emit "response", @res + @urlStream.emit "end" + @fileStream.emit "finish" + + + it "should use override domain when filestoreDomainOveride is set", (done)-> + @settings.filestoreDomainOveride = "192.11.11.11" + @UrlFetcher.pipeUrlToFile @url, @path, => + @request.get.args[0][0].url.should.equal "192.11.11.11/file/here?query=string" + done() + @res = statusCode: 200 + @urlStream.emit "response", @res + @urlStream.emit "end" + @fileStream.emit "finish" + describe "pipeUrlToFile", -> beforeEach (done)-> @path = "/path/to/file/on/disk" From a75cec7d52d5a0cdde7a17cfae8f04e2a0819d97 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 5 Jul 2018 15:07:07 +0100 Subject: [PATCH 354/709] added maint down endpoint --- app.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app.coffee b/app.coffee index cf01b479..88bb74b3 100644 --- a/app.coffee +++ b/app.coffee @@ -174,9 +174,6 @@ os = require "os" STATE = "up" -process.on "SIGHUP", -> - console.log "got SIGHUP event" - STATE = "down" loadTcpServer = net.createServer (socket) -> socket.on "error", (err)-> @@ -217,6 +214,12 @@ loadHttpServer.post "/state/down", (req, res, next) -> logger.info "getting message to set server to down" res.sendStatus 204 +loadHttpServer.post "/state/maint", (req, res, next) -> + STATE = "maint" + logger.info "getting message to set server to maint" + res.sendStatus 204 + + port = (Settings.internal?.clsi?.port or 3013) host = (Settings.internal?.clsi?.host or "localhost") From c1277e9f22794b77123c356337046cc3853e0c5b Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 6 Jul 2018 15:08:38 +0100 Subject: [PATCH 355/709] Use our experimental metrics --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79f9c806..0edb1160 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.8.1-beta", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 97716365afae71a2cd0603a34154e0faa2c89567 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 12 Jul 2018 11:22:02 +0100 Subject: [PATCH 356/709] Depend on metrics v1.8.1 for remote StatsD host --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0edb1160..ab34cda8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.8.1-beta", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.8.1", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From a960614eb4bde3667b36363326339e769d2f5643 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 13 Jul 2018 10:37:22 +0100 Subject: [PATCH 357/709] added texliveImageNameOveride --- app/coffee/RequestParser.coffee | 6 ++++++ config/settings.defaults.coffee | 1 + test/unit/coffee/RequestParserTests.coffee | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 596b5299..d375e9e3 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -1,3 +1,5 @@ +settings = require("settings-sharelatex") + module.exports = RequestParser = VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"] MAX_TIMEOUT: 300 @@ -67,6 +69,10 @@ module.exports = RequestParser = sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath) response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath) + if settings.texliveImageNameOveride? + tag = compile.options.imageName.split(":")[1] + response.imageName = "#{settings.texliveImageNameOveride}:#{tag}" + for resource in response.resources if resource.path == originalRootResourcePath resource.path = sanitizedRootResourcePath diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 081ded81..1f3fe8be 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -33,6 +33,7 @@ module.exports = project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] + texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"] if process.env["DOCKER_RUNNER"] diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index 0b420b36..04405ac6 100644 --- a/test/unit/coffee/RequestParserTests.coffee +++ b/test/unit/coffee/RequestParserTests.coffee @@ -16,10 +16,12 @@ describe "RequestParser", -> compile: token: "token-123" options: + imageName: "basicImageName/here:2017-1" compiler: "pdflatex" timeout: 42 resources: [] - @RequestParser = SandboxedModule.require modulePath + @RequestParser = SandboxedModule.require modulePath, requires: + "settings-sharelatex": @settings = {} afterEach -> tk.reset() @@ -57,6 +59,21 @@ describe "RequestParser", -> it "should set the compiler to pdflatex by default", -> @data.compiler.should.equal "pdflatex" + describe "with imageName set", -> + beforeEach -> + @RequestParser.parse @validRequest, (error, @data) => + + it "should set the imageName", -> + @data.imageName.should.equal "basicImageName/here:2017-1" + + describe "with texliveImageNameOveride set", -> + beforeEach -> + @settings.texliveImageNameOveride = "usethisoveride/overhere" + @RequestParser.parse @validRequest, (error, @data) => + + it "should override the image path", -> + @data.imageName.should.equal "usethisoveride/overhere:2017-1" + describe "without a timeout specified", -> beforeEach -> delete @validRequest.compile.options.timeout From 354585217391bf46cac7ec97b1d8908e39b04ca1 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 13 Jul 2018 11:46:37 +0100 Subject: [PATCH 358/709] quick hack to overright image name further down stack --- app/coffee/DockerRunner.coffee | 4 ++++ app/coffee/RequestParser.coffee | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 635243fd..f48e904b 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -38,6 +38,10 @@ module.exports = DockerRunner = if !image? image = Settings.clsi.docker.image + if Settings.texliveImageNameOveride? + tag = image.split(":")[1] + image = "#{Settings.texliveImageNameOveride}:#{tag}" + options = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment) fingerprint = DockerRunner._fingerprintContainer(options) options.name = name = "project-#{project_id}-#{fingerprint}" diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index d375e9e3..78246169 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -69,7 +69,7 @@ module.exports = RequestParser = sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath) response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath) - if settings.texliveImageNameOveride? + if settings.texliveImageNameOveride? and compile.options?.imageName? tag = compile.options.imageName.split(":")[1] response.imageName = "#{settings.texliveImageNameOveride}:#{tag}" From 8d846f64a9c946a606a908d81bfd4fe9dd43ff0d Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 13 Jul 2018 11:52:49 +0100 Subject: [PATCH 359/709] move texliveImageNameOveride further down request so it works for compile tests --- app/coffee/RequestParser.coffee | 4 ---- test/unit/coffee/DockerRunnerTests.coffee | 10 ++++++++++ test/unit/coffee/RequestParserTests.coffee | 8 -------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 78246169..cabbac38 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -69,10 +69,6 @@ module.exports = RequestParser = sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath) response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath) - if settings.texliveImageNameOveride? and compile.options?.imageName? - tag = compile.options.imageName.split(":")[1] - response.imageName = "#{settings.texliveImageNameOveride}:#{tag}" - for resource in response.resources if resource.path == originalRootResourcePath resource.path = sanitizedRootResourcePath diff --git a/test/unit/coffee/DockerRunnerTests.coffee b/test/unit/coffee/DockerRunnerTests.coffee index 5a697e2b..456f52b4 100644 --- a/test/unit/coffee/DockerRunnerTests.coffee +++ b/test/unit/coffee/DockerRunnerTests.coffee @@ -135,6 +135,16 @@ describe "DockerRunner", -> @DockerRunner._getContainerOptions .calledWith(@command_with_dir, @defaultImage, @volumes, @timeout) .should.equal true + + describe "with image override", -> + beforeEach -> + @Settings.texliveImageNameOveride = "overrideimage/here" + @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") + @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback + + it "should use the override and keep the tag", -> + image = @DockerRunner._getContainerOptions.args[0][1] + image.should.equal "overrideimage/here:2016.2" describe "_runAndWaitForContainer", -> beforeEach -> diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index 04405ac6..f63bc55e 100644 --- a/test/unit/coffee/RequestParserTests.coffee +++ b/test/unit/coffee/RequestParserTests.coffee @@ -66,14 +66,6 @@ describe "RequestParser", -> it "should set the imageName", -> @data.imageName.should.equal "basicImageName/here:2017-1" - describe "with texliveImageNameOveride set", -> - beforeEach -> - @settings.texliveImageNameOveride = "usethisoveride/overhere" - @RequestParser.parse @validRequest, (error, @data) => - - it "should override the image path", -> - @data.imageName.should.equal "usethisoveride/overhere:2017-1" - describe "without a timeout specified", -> beforeEach -> delete @validRequest.compile.options.timeout From dd015a05cb024ec3f22d407c3bffab524abb188e Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 16 Jul 2018 15:38:23 +0100 Subject: [PATCH 360/709] remove express header --- app.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app.coffee b/app.coffee index 88bb74b3..7855e092 100644 --- a/app.coffee +++ b/app.coffee @@ -35,6 +35,7 @@ TIMEOUT = 6 * 60 * 1000 app.use (req, res, next) -> req.setTimeout TIMEOUT res.setTimeout TIMEOUT + res.removeHeader("X-Powered-By") next() app.param 'project_id', (req, res, next, project_id) -> From bcb87620b58aea6d7ae5f450bcd56994f3d8817e Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 16 Jul 2018 17:25:14 +0100 Subject: [PATCH 361/709] change override to leave image name so it works for wl_texlive --- app/coffee/DockerRunner.coffee | 4 ++-- test/unit/coffee/DockerRunnerTests.coffee | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index f48e904b..69a3df97 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -39,8 +39,8 @@ module.exports = DockerRunner = image = Settings.clsi.docker.image if Settings.texliveImageNameOveride? - tag = image.split(":")[1] - image = "#{Settings.texliveImageNameOveride}:#{tag}" + img = image.split("/") + image = "#{Settings.texliveImageNameOveride}/#{img[2]}" options = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment) fingerprint = DockerRunner._fingerprintContainer(options) diff --git a/test/unit/coffee/DockerRunnerTests.coffee b/test/unit/coffee/DockerRunnerTests.coffee index 456f52b4..307ffde1 100644 --- a/test/unit/coffee/DockerRunnerTests.coffee +++ b/test/unit/coffee/DockerRunnerTests.coffee @@ -138,13 +138,13 @@ describe "DockerRunner", -> describe "with image override", -> beforeEach -> - @Settings.texliveImageNameOveride = "overrideimage/here" + @Settings.texliveImageNameOveride = "overrideimage.com/something" @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback it "should use the override and keep the tag", -> image = @DockerRunner._getContainerOptions.args[0][1] - image.should.equal "overrideimage/here:2016.2" + image.should.equal "overrideimage.com/something/image:2016.2" describe "_runAndWaitForContainer", -> beforeEach -> From 4830e9f7852401204b056713198d1615dde7b5d9 Mon Sep 17 00:00:00 2001 From: Brian Gough <bjg@network-theory.co.uk> Date: Tue, 17 Jul 2018 10:41:10 +0100 Subject: [PATCH 362/709] allow prune to fail to prevent build from terminating --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0c289e15..ce7367db 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -54,7 +54,7 @@ pipeline { steps { sh 'mkdir -p compiles cache' // Not yet running, due to volumes/sibling containers - sh 'docker container prune -f' + sh 'docker container prune -f || true' sh 'docker pull $TEXLIVE_IMAGE' sh 'docker pull sharelatex/acceptance-test-runner:clsi-6.11.2' sh 'docker run --rm -e SIBLING_CONTAINER_USER=root -e SANDBOXED_COMPILES_HOST_DIR=$(pwd)/compiles -e SANDBOXED_COMPILES_SIBLING_CONTAINERS=true -e TEXLIVE_IMAGE=$TEXLIVE_IMAGE -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/app sharelatex/acceptance-test-runner:clsi-6.11.2' From fb00098fc025191752bc533069b64a53cf27de41 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 17 Jul 2018 12:10:08 +0100 Subject: [PATCH 363/709] Bump build script to 1.1.8, drop csh-gcdm-test and csh-staging repos --- Jenkinsfile | 56 +++++++++++++++++++++++++++++++++++-------- Makefile | 8 +++---- docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- package.json | 2 +- 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index efe84483..6db5e298 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,12 +3,33 @@ String cron_string = BRANCH_NAME == "master" ? "@daily" : "" pipeline { agent any + environment { + GIT_PROJECT = "clsi-sharelatex" + JENKINS_WORKFLOW = "clsi-sharelatex" + TARGET_URL = "${env.JENKINS_URL}blue/organizations/jenkins/${JENKINS_WORKFLOW}/detail/$BRANCH_NAME/$BUILD_NUMBER/pipeline" + GIT_API_URL = "https://api.github.com/repos/sharelatex/${GIT_PROJECT}/statuses/$GIT_COMMIT" + } + triggers { pollSCM('* * * * *') cron(cron_string) } stages { + stage('Install') { + steps { + withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { + sh "curl $GIT_API_URL \ + --data '{ \ + \"state\" : \"pending\", \ + \"target_url\": \"$TARGET_URL\", \ + \"description\": \"Your build is underway\", \ + \"context\": \"ci/jenkins\" }' \ + -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" + } + } + } + stage('Build') { steps { sh 'make build' @@ -30,17 +51,11 @@ pipeline { stage('Package and publish build') { steps { - withCredentials([file(credentialsId: 'gcr.io_csh-gcdm-test', variable: 'DOCKER_REPO_KEY_PATH')]) { - sh 'docker login -u _json_key --password-stdin https://gcr.io/csh-gcdm-test < ${DOCKER_REPO_KEY_PATH}' + withCredentials([file(credentialsId: 'gcr.io_cr-test2', variable: 'DOCKER_REPO_KEY_PATH')]) { + sh 'docker login -u _json_key --password-stdin https://gcr.io/cr-test2 < ${DOCKER_REPO_KEY_PATH}' } - sh 'DOCKER_REPO=gcr.io/csh-gcdm-test make publish' - sh 'docker logout https://gcr.io/csh-gcdm-test' - - withCredentials([file(credentialsId: 'gcr.io_csh-staging', variable: 'DOCKER_REPO_KEY_PATH')]) { - sh 'docker login -u _json_key --password-stdin https://gcr.io/csh-staging < ${DOCKER_REPO_KEY_PATH}' - } - sh 'DOCKER_REPO=gcr.io/csh-staging make publish' - sh 'docker logout https://gcr.io/csh-staging' + sh 'DOCKER_REPO=gcr.io/cr-test2 make publish' + sh 'docker logout https://gcr.io/cr-test2' } } @@ -62,11 +77,32 @@ pipeline { sh 'make clean' } + success { + withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { + sh "curl $GIT_API_URL \ + --data '{ \ + \"state\" : \"success\", \ + \"target_url\": \"$TARGET_URL\", \ + \"description\": \"Your build succeeded!\", \ + \"context\": \"ci/jenkins\" }' \ + -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" + } + } + failure { mail(from: "${EMAIL_ALERT_FROM}", to: "${EMAIL_ALERT_TO}", subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", body: "Build: ${BUILD_URL}") + withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { + sh "curl $GIT_API_URL \ + --data '{ \ + \"state\" : \"failure\", \ + \"target_url\": \"$TARGET_URL\", \ + \"description\": \"Your build failed\", \ + \"context\": \"ci/jenkins\" }' \ + -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" + } } } diff --git a/Makefile b/Makefile index b7c96a2b..68e3c1cd 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.7 +# Version: 1.1.8 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -16,8 +16,7 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ clean: docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - docker rmi gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - docker rmi gcr.io/csh-staging/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + docker rmi gcr.io/cr-test2/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) rm -f app.js rm -rf app/js rm -rf test/unit/js @@ -38,8 +37,7 @@ test_acceptance_pre_run: @[ ! -f test/acceptance/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ - --tag gcr.io/csh-gcdm-test/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ - --tag gcr.io/csh-staging/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag gcr.io/cr-test2/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ . publish: diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 9f3d54d1..e8850b93 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.7 +# Version: 1.1.8 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index 4c1cfc73..f47658d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.7 +# Version: 1.1.8 version: "2" diff --git a/package.json b/package.json index ab34cda8..bafda195 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests", "nodemon": "nodemon --config nodemon.json", - "compile:smoke_tests": "[ ! -e test/smoke/coffee] && echo 'No smoke tests to compile' || coffee -o test/smoke/js -c test/smoke/coffee" + "compile:smoke_tests": "[ ! -e test/smoke/coffee ] && echo 'No smoke tests to compile' || coffee -o test/smoke/js -c test/smoke/coffee" }, "author": "James Allen <james@sharelatex.com>", "dependencies": { From 3e26efe06fd59bf46c0089872174d0734c1944ec Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 17 Jul 2018 12:50:33 +0100 Subject: [PATCH 364/709] add PRAGMA journal_mode=WAL; --- app/coffee/db.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index f32cdf7a..c21da6d5 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -14,6 +14,8 @@ sequelize = new Sequelize( options ) +sequelize.query("PRAGMA journal_mode=WAL;") + module.exports = UrlCache: sequelize.define("UrlCache", { url: Sequelize.STRING From 2b6032b24960de525e2517064ccdf31fce4eb322 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 17 Jul 2018 12:53:07 +0100 Subject: [PATCH 365/709] only set wal for sqlite --- app/coffee/db.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index c21da6d5..c8866e0c 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -14,7 +14,8 @@ sequelize = new Sequelize( options ) -sequelize.query("PRAGMA journal_mode=WAL;") +if Settings.mysql.clsi.dialect == "sqlite" + sequelize.query("PRAGMA journal_mode=WAL;") module.exports = UrlCache: sequelize.define("UrlCache", { From 465dc31e75fde24df2752cdbe91f501c43f0f539 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Wed, 18 Jul 2018 11:32:41 +0100 Subject: [PATCH 366/709] Push images to overleaf-ops --- Jenkinsfile | 8 ++++---- Makefile | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6db5e298..d82360d4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -51,11 +51,11 @@ pipeline { stage('Package and publish build') { steps { - withCredentials([file(credentialsId: 'gcr.io_cr-test2', variable: 'DOCKER_REPO_KEY_PATH')]) { - sh 'docker login -u _json_key --password-stdin https://gcr.io/cr-test2 < ${DOCKER_REPO_KEY_PATH}' + withCredentials([file(credentialsId: 'gcr.io_overleaf-ops', variable: 'DOCKER_REPO_KEY_PATH')]) { + sh 'docker login -u _json_key --password-stdin https://gcr.io/overleaf-ops < ${DOCKER_REPO_KEY_PATH}' } - sh 'DOCKER_REPO=gcr.io/cr-test2 make publish' - sh 'docker logout https://gcr.io/cr-test2' + sh 'DOCKER_REPO=gcr.io/overleaf-ops make publish' + sh 'docker logout https://gcr.io/overleaf-ops' } } diff --git a/Makefile b/Makefile index 68e3c1cd..6daee1be 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ clean: docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - docker rmi gcr.io/cr-test2/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) rm -f app.js rm -rf app/js rm -rf test/unit/js @@ -37,7 +37,7 @@ test_acceptance_pre_run: @[ ! -f test/acceptance/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ - --tag gcr.io/cr-test2/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ . publish: From 67d34fdaf05804bc718dd7ac42e22c43272067f0 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 26 Jul 2018 16:12:26 +0100 Subject: [PATCH 367/709] dd wal logging --- app/coffee/db.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index c8866e0c..c0bb1cac 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -15,6 +15,7 @@ sequelize = new Sequelize( ) if Settings.mysql.clsi.dialect == "sqlite" + logger.log "running PRAGMA journal_mode=WAL;" sequelize.query("PRAGMA journal_mode=WAL;") module.exports = From 0eeee4284d35f59b67604a5cd3342ff5478607fc Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 11:25:28 +0100 Subject: [PATCH 368/709] bump retried and package versions --- config/settings.defaults.coffee | 5 +++++ package.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 1f3fe8be..7a2e7cfe 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -10,6 +10,11 @@ module.exports = password: null dialect: "sqlite" storage: process.env["SQLITE_PATH"] or Path.resolve(__dirname + "/../db.sqlite") + pool: + max: 1 + min: 0 + retry: + max: 10 path: compilesDir: Path.resolve(__dirname + "/../compiles") diff --git a/package.json b/package.json index bafda195..c34b93cb 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,10 @@ "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", - "sequelize": "^2.1.3", + "sequelize": "4.38.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "^3.1.13", + "sqlite3": "4.0.2", "underscore": "^1.8.2", "v8-profiler": "^5.2.4", "wrench": "~1.5.4" From f802717cb5802a3ff9d4d643695201fb5db332aa Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 14:04:33 +0100 Subject: [PATCH 369/709] remove password from clsi for sql sequalise fails when it is set to null --- .gitignore | 2 + app/coffee/db.coffee | 6 + config/settings.defaults.coffee | 3 +- package-lock.json | 2618 +++++++++++++++++++++++++++++-- package.json | 4 +- 5 files changed, 2465 insertions(+), 168 deletions(-) diff --git a/.gitignore b/.gitignore index 21c1ecae..7fb78eef 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,7 @@ app.js cache .vagrant db.sqlite +db.sqlite-wal +db.sqlite-shm config/* npm-debug.log diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index c0bb1cac..a24377e7 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -42,4 +42,10 @@ module.exports = sync: () -> logger.log dbPath:Settings.mysql.clsi.storage, "syncing db schema" sequelize.sync() + .then(-> + logger.log "db sync complete" + ).catch((err)-> + console.log err, "error syncing" + ) + diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 7a2e7cfe..a2bf5930 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -7,12 +7,11 @@ module.exports = clsi: database: "clsi" username: "clsi" - password: null dialect: "sqlite" storage: process.env["SQLITE_PATH"] or Path.resolve(__dirname + "/../db.sqlite") pool: max: 1 - min: 0 + min: 1 retry: max: 10 diff --git a/package-lock.json b/package-lock.json index d00b345e..d58c1d73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,11 +4,157 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/geojson": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", + "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" + }, + "@types/node": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.4.tgz", + "integrity": "sha512-8TqvB0ReZWwtcd3LXq3YSrBoLyXFgBX/sBZfGye9+YS8zH7/g+i6QRIuiDmwBoTzcQ/pk89nZYTYU4c5akKkzw==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", + "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "1.7.0", + "underscore.string": "2.4.0" + }, + "dependencies": { + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", + "dev": true + }, + "async": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", + "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "optional": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "bignumber.js": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", + "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, "body-parser": { "version": "1.18.2", @@ -156,21 +302,325 @@ } } }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", - "optional": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" } }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "1.1.0", + "buffer-fill": "1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "bunyan": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", + "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", + "dev": true, + "requires": { + "mv": "2.1.1" + } + }, + "buster-core": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", + "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", + "dev": true + }, + "buster-format": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", + "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", + "dev": true, + "requires": { + "buster-core": "0.6.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", + "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", + "dev": true, + "requires": { + "assertion-error": "1.0.0", + "deep-eql": "0.1.3" + } + }, + "chalk": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", + "integrity": "sha1-HJhDdzfxGZ68wdTEj9Qbn5yOjyM=", + "dev": true, + "requires": { + "ansi-styles": "0.2.0", + "has-color": "0.1.7" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + }, + "cls-bluebird": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", + "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", + "requires": { + "is-bluebird": "1.0.2", + "shimmer": "1.2.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "coffee-script": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "optional": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.0.6", + "typedarray": "0.0.6" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" + }, + "docker-modem": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.6.tgz", + "integrity": "sha512-kDwWa5QaiVMB8Orbb7nXdGdwEZHKfEm7iPwglXe1KorImMpmGNlhC7A5LG0p8rrCcz1J4kJhq/o63lFjDdj8rQ==", + "requires": { + "debug": "3.1.0", + "JSONStream": "1.3.2", + "readable-stream": "1.0.34", + "split-ca": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "dockerode": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.5.tgz", + "integrity": "sha512-H3HX18xKmy51wqpPHvGDwPOotJMy9l/AWfiaVu4imrgBGr384rINEB2FwTwoYU++krkZjseVYyiVK8CnRz2tkw==", + "requires": { + "concat-stream": "1.5.2", + "docker-modem": "1.0.6", + "tar-fs": "1.12.0" + } + }, + "dottie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.0.tgz", + "integrity": "sha1-2hkZgci41xPKARXViYzzl8Lw3dA=" }, "dtrace-provider": { "version": "0.6.0", @@ -178,9 +628,51 @@ "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", "optional": true, "requires": { - "nan": "2.8.0" + "nan": "2.10.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "0.1.1", + "safer-buffer": "2.1.2" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" } }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, "express": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", @@ -499,214 +991,2012 @@ } } }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "optional": true, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "glob": "3.2.11", + "lodash": "2.4.2" }, "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "optional": true, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, "requires": { - "wrappy": "1.0.2" + "inherits": "2.0.3", + "minimatch": "0.3.0" } }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "optional": true + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } } } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "optional": true, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - }, - "dependencies": { - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "optional": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.19" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", + "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", + "requires": { + "graceful-fs": "3.0.11", + "jsonfile": "2.4.0", + "rimraf": "2.6.2" + } + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha1-BsJ3IYRU7CiN93raVKA7hwKqy50=", + "requires": { + "minipass": "2.3.3" } }, - "load-balancer-agent-sharelatex": { - "version": "git+https://github.com/sharelatex/load-balancer-agent-sharelatex.git#241a128c1e9fdec384186061b27704c8891d98ef", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "requires": { - "express": "4.16.2", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#405cf1350ca5ae5f7bb1e7091e28d5aa3aaaa72c", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#b4fb8404c5de571d029bf4c29e96a60b21206f94" + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" }, "dependencies": { - "bunyan": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", - "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", - "requires": { - "dtrace-provider": "0.6.0", - "mv": "2.1.1", - "safe-json-stringify": "1.0.4" - } - }, - "coffee-script": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", - "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" - }, - "cookie": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz", - "integrity": "sha1-kOtGndzpBchm3mh+/EMTHYgB+dA=" - }, - "logger-sharelatex": { - "version": "git+https://github.com/sharelatex/logger-sharelatex.git#405cf1350ca5ae5f7bb1e7091e28d5aa3aaaa72c", - "requires": { - "bunyan": "1.5.1", - "coffee-script": "1.4.0", - "raven": "0.8.1" - } - }, - "lsmod": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz", - "integrity": "sha1-F+E9ThrpF1DqVlNUjNiecUetAkQ=" + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, - "raven": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz", - "integrity": "sha1-UVk7tlnHcnjc00gitlq+d7dRuvU=", - "requires": { - "cookie": "0.1.0", - "lsmod": "0.0.3", - "node-uuid": "1.4.8", - "stack-trace": "0.0.7" - } - }, - "settings-sharelatex": { - "version": "git+https://github.com/sharelatex/settings-sharelatex.git#b4fb8404c5de571d029bf4c29e96a60b21206f94", + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { - "coffee-script": "1.6.0" - }, - "dependencies": { - "coffee-script": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" - } + "minimist": "0.0.8" } - }, - "stack-trace": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.7.tgz", - "integrity": "sha1-xy4Il0T8Nln1CM3ONiGvVjTsD/8=" } } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.3" + } + }, + "generic-pool": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz", + "integrity": "sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag==" + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "requires": { + "natives": "1.1.4" + } + }, + "growl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" + }, + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "requires": { + "async": "0.1.22", + "coffee-script": "1.3.3", + "colors": "0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "0.4.14", + "exit": "0.1.2", + "findup-sync": "0.1.3", + "getobject": "0.1.0", + "glob": "3.1.21", + "grunt-legacy-log": "0.1.3", + "grunt-legacy-util": "0.2.0", + "hooker": "0.2.3", + "iconv-lite": "0.2.11", + "js-yaml": "2.0.5", + "lodash": "0.9.2", + "minimatch": "0.2.14", + "nopt": "1.0.10", + "rimraf": "2.2.8", + "underscore.string": "2.2.1", + "which": "1.0.9" + }, + "dependencies": { + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "grunt-bunyan": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", + "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", + "dev": true, + "requires": { + "lodash": "2.4.2" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "grunt-contrib-clean": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", + "integrity": "sha1-9T397ghJsce0Dp67umn0jExgecU=", + "dev": true, + "requires": { + "rimraf": "2.2.8" + }, + "dependencies": { + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "grunt-contrib-coffee": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", + "integrity": "sha1-ixIme3TnM4sfKcW4txj7n4mYLxM=", + "dev": true, + "requires": { + "coffee-script": "1.6.3" + }, + "dependencies": { + "coffee-script": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", + "integrity": "sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4=", + "dev": true + } + } + }, + "grunt-execute": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", + "integrity": "sha1-yX64lDYS/vu3L749Mu+VIzxfouk=", + "dev": true + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "0.6.2", + "grunt-legacy-log-utils": "0.1.1", + "hooker": "0.2.3", + "lodash": "2.4.2", + "underscore.string": "2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "0.6.2", + "lodash": "2.4.2", + "underscore.string": "2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "0.1.22", + "exit": "0.1.2", + "getobject": "0.1.0", + "hooker": "0.2.3", + "lodash": "0.9.2", + "underscore.string": "2.2.1", + "which": "1.0.9" + }, + "dependencies": { + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + } + } + }, + "grunt-mkdir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz", + "integrity": "sha1-c+GiasJKCFljY/TdlUsNMkheWOk=" + }, + "grunt-mocha-test": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", + "integrity": "sha1-emGEuYhg0Phb3qrWvqob199bvus=", + "dev": true, + "requires": { + "mocha": "1.14.0" + }, + "dependencies": { + "glob": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "dev": true, + "requires": { + "graceful-fs": "2.0.3", + "inherits": "2.0.3", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "mocha": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", + "integrity": "sha1-cT223FAAGRqdA1gZXQkIeQ7LYVc=", + "dev": true, + "requires": { + "commander": "2.0.0", + "debug": "2.6.9", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.0", + "jade": "0.26.3", + "mkdirp": "0.3.5" + } + } + } + }, + "grunt-shell": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", + "integrity": "sha1-5KbRuSkSd2/ZOimcX2zGTpUlNlw=", + "dev": true, + "requires": { + "chalk": "0.3.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "heapdump": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.9.tgz", + "integrity": "sha1-A8dOsN9dZ74Jgug0KbqcnSs7f3g=" + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha1-qD5i59JyrA47VRqqgoMaGbafgvg=", + "requires": { + "minimatch": "3.0.4" + } + }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" + }, + "is-bluebird": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" + } + } + }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "0.1.16", + "esprima": "1.0.4" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "4.1.11" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "optional": true + } + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, + "JSONStream": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", + "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "requires": { + "signal-exit": "3.0.2" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "logger-sharelatex": { + "version": "git+https://github.com/sharelatex/logger-sharelatex.git#9ee7b52eb2bbd8fcbb1e2c708587c1e93fd4c733", + "requires": { + "bunyan": "1.5.1", + "coffee-script": "1.4.0", + "raven": "1.2.1" + }, + "dependencies": { + "bunyan": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", + "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", + "requires": { + "dtrace-provider": "0.6.0", + "mv": "2.1.1", + "safe-json-stringify": "1.2.0" + } + }, + "coffee-script": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", + "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" + } + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "lsmod": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", + "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" + }, + "lynx": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", + "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", + "requires": { + "mersenne": "0.0.4", + "statsd-parser": "0.0.4" + } + }, + "mersenne": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" + }, + "metrics-sharelatex": { + "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#e5356366b5b83997c8e1645b2e936af453381517", + "requires": { + "coffee-script": "1.6.0", + "lynx": "0.1.1", + "underscore": "1.6.0" + }, + "dependencies": { + "lynx": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", + "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", + "requires": { + "mersenne": "0.0.4", + "statsd-parser": "0.0.4" + } + }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" + } + } + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + }, + "mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "requires": { + "mime-db": "1.35.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "minipass": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.3.tgz", + "integrity": "sha1-p9zIt7gz9dNodZzOVE3MtV9Q8jM=", + "requires": { + "safe-buffer": "5.1.2", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha1-EeE2WM5GvDpwomeqxYNZ0eDCnOs=", + "requires": { + "minipass": "2.3.3" + } + }, + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" + }, + "mocha": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, + "moment-timezone": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz", + "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==", + "requires": { + "moment": "2.22.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", "optional": true, "requires": { - "brace-expansion": "1.1.11" + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "6.0.4" + } + } + } + }, + "mysql": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", + "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", + "requires": { + "bignumber.js": "2.0.7", + "readable-stream": "1.1.14", + "require-all": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + }, + "natives": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.4.tgz", + "integrity": "sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg==" + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "needle": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.1.tgz", + "integrity": "sha1-teMlvTqujCZ4kC+ilvcpRV0dOn0=", + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.23", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.1", + "nopt": "4.0.1", + "npm-packlist": "1.1.11", + "npmlog": "4.1.2", + "rc": "1.2.8", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.4" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + } + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha1-fnFwPZc68zcKlZG6/jpjrKC+Iwg=" + }, + "npm-packlist": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", + "integrity": "sha1-hOjGg8vnhn00sdNX2JPOKeKKAt4=", + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "requires": { + "are-we-there-yet": "1.1.5", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" } }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "optional": true, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "raven": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", + "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", + "requires": { + "cookie": "0.3.1", + "json-stringify-safe": "5.0.1", + "lsmod": "1.0.0", + "stack-trace": "0.0.9", + "uuid": "3.0.0" + }, + "dependencies": { + "uuid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", + "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "0.6.0", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" }, "dependencies": { "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.19", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + } + }, + "require-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", + "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" + }, + "require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", + "dev": true + }, + "retry-as-promised": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.3.2.tgz", + "integrity": "sha1-zZdO5P2bX+A8vzGHHuSCIcB3N7c=", + "requires": { + "bluebird": "3.5.1", + "debug": "2.6.9" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sandboxed-module": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", + "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", + "dev": true, + "requires": { + "require-like": "0.1.2", + "stack-trace": "0.0.6" + }, + "dependencies": { + "stack-trace": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", + "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=" + }, + "sequelize": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.38.0.tgz", + "integrity": "sha512-ZCcV2HuzU+03xunWgVeyXnPa/RYY5D2U/WUNpq+xF8VmDTLnSDsHl+pEwmiWrpZD7KdBqDczCeTgjToYyVzYQg==", + "requires": { + "bluebird": "3.5.1", + "cls-bluebird": "2.1.0", + "debug": "3.1.0", + "depd": "1.1.2", + "dottie": "2.0.0", + "generic-pool": "3.4.2", + "inflection": "1.12.0", + "lodash": "4.17.10", + "moment": "2.22.2", + "moment-timezone": "0.5.21", + "retry-as-promised": "2.3.2", + "semver": "5.5.0", + "terraformer-wkt-parser": "1.2.0", + "toposort-class": "1.0.1", + "uuid": "3.3.2", + "validator": "10.4.0", + "wkx": "0.4.5" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "settings-sharelatex": { + "version": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", + "requires": { + "coffee-script": "1.6.0" + } + }, + "shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-xTCx2vohXC2EWWDqY/zb4+5Mu28D+HYNSOuFzsyRDRvI/e1ICb69afwaUwfjr+25ZXldbOLyp+iDUZHq8UnTag==" + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sinon": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", + "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", + "dev": true, + "requires": { + "buster-format": "0.5.6" + } + }, + "smoke-test-sharelatex": { + "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", + "requires": { + "mocha": "1.17.1" + }, + "dependencies": { + "glob": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "requires": { + "graceful-fs": "2.0.3", + "inherits": "2.0.3", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } }, + "mocha": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", + "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", + "requires": { + "commander": "2.0.0", + "debug": "2.6.9", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.0", + "jade": "0.26.3", + "mkdirp": "0.3.5" + } + } + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, + "sqlite3": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.2.tgz", + "integrity": "sha512-51ferIRwYOhzUEtogqOa/y9supADlAht98bF/gbIi6WkzRJX6Yioldxbzj1MV4yV+LgdKD/kkHwFTeFXOG4htA==", + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.10.3", + "request": "2.87.0" + }, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha1-MvACNc0I1IK00NaNuTqCnA7VdW4=", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.19", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + } + } + } + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.2", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.2", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" + } + }, + "stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" + }, + "statsd-parser": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", + "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "tar": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.4.tgz", + "integrity": "sha512-mq9ixIYfNF9SK0IS/h2HKMu8Q2iaCuhDDsZhdEag/FHv8fOaYld4vN7ouMgcSSt5WKZzPs8atclTcJm36OTh4w==", + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.3.3", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.2", + "yallist": "3.0.2" + }, + "dependencies": { "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, "requires": { "minimist": "0.0.8" } } } }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", - "optional": true + "tar-fs": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", + "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", + "requires": { + "mkdirp": "0.5.1", + "pump": "1.0.3", + "tar-stream": "1.6.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + } + } }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "optional": true + "tar-pack": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.6", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + }, + "dependencies": { + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + } + } }, - "node-uuid": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + "tar-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", + "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "requires": { + "bl": "1.2.2", + "buffer-alloc": "1.2.0", + "end-of-stream": "1.4.1", + "fs-constants": "1.0.0", + "readable-stream": "2.3.6", + "to-buffer": "1.1.1", + "xtend": "4.0.1" + } }, - "path-is-absolute": { + "terraformer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.9.tgz", + "integrity": "sha512-YlmQ1fsMWTkKGDGibCRWgmLzrpDRUr63Q025LJ/taYQ6j1Yb8q9McKF7NBi6ACAyUXO6F/bl9w6v4MY307y5Ag==", + "requires": { + "@types/geojson": "1.0.6" + } + }, + "terraformer-wkt-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz", + "integrity": "sha512-QU3iA54St5lF8Za1jg1oj4NYc8sn5tCZ08aNSWDeGzrsaV48eZk1iAVWasxhNspYBoCqdHuoot1pUTUrE1AJ4w==", + "requires": { + "@types/geojson": "1.0.6", + "terraformer": "1.0.9" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "timekeeper": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", + "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", + "dev": true + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, + "toposort-class": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "optional": true + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "optional": true, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha1-7GDO44rGdQY//JelwYlwV47oNlU=", "requires": { - "glob": "6.0.4" + "punycode": "1.4.1" } }, - "safe-json-stringify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", - "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true + }, + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + }, + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8-profiler": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", + "integrity": "sha1-6DgcvrtbX9DKjSsJ9qAYGhWNs00=", + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.6.39" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.19" + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.14.2" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "node-pre-gyp": { + "version": "0.6.39", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", + "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", + "requires": { + "detect-libc": "1.0.3", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.8", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "2.2.1", + "tar-pack": "3.4.1" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.19", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.2", + "stringstream": "0.0.6", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + } + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + } + } + }, + "validator": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.4.0.tgz", + "integrity": "sha512-Q/wBy3LB1uOyssgNlXSRmaf22NxjvDNZM2MtIQ4jaEOAB61xsh1TQxsq1CgzUMBV1lDrVMogIh8GjG1DYW0zLg==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "1.0.2" + } + }, + "wkx": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.5.tgz", + "integrity": "sha512-01dloEcJZAJabLO5XdcRgqdKpmnxS0zIT02LhkdWOZX2Zs2tPM6hlZ4XG9tWaWur1Qd1OO4kJxUbe2+5BofvnA==", + "requires": { + "@types/node": "10.5.4" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "wrench": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", + "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } } diff --git a/package.json b/package.json index c34b93cb..f2183ef9 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,10 @@ "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", - "sequelize": "4.38.0", + "sequelize": "^4.38.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "4.0.2", + "sqlite3": "^4.0.2", "underscore": "^1.8.2", "v8-profiler": "^5.2.4", "wrench": "~1.5.4" From c490479a1a353e86f698fa75e1b0ee1cf7baaaef Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 15:11:41 +0100 Subject: [PATCH 370/709] remove some console.logs --- app.coffee | 1 - app/coffee/ResourceWriter.coffee | 2 -- app/coffee/UrlFetcher.coffee | 1 - 3 files changed, 4 deletions(-) diff --git a/app.coffee b/app.coffee index 7855e092..8b5c779d 100644 --- a/app.coffee +++ b/app.coffee @@ -133,7 +133,6 @@ resCacher = if Settings.smokeTest do runSmokeTest = -> logger.log("running smoke tests") - console.log(__dirname, __filename) smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher) setTimeout(runSmokeTest, 30 * 1000) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 90ada043..0c9f7180 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -121,11 +121,9 @@ module.exports = ResourceWriter = callback() #try and continue compiling even if http resource can not be downloaded at this time else process = require("process") - console.log "writing file out", path, process.getuid() fs.writeFile path, resource.content, callback try result = fs.lstatSync(path) - console.log "path stats", result catch e checkPath: (basePath, resourcePath, callback) -> diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.coffee index 58645f00..da10859f 100644 --- a/app/coffee/UrlFetcher.coffee +++ b/app/coffee/UrlFetcher.coffee @@ -36,7 +36,6 @@ module.exports = UrlFetcher = logger.log url:url, filePath: filePath, "finished downloading file into cache" urlStream.on "response", (res) -> - console.log if res.statusCode >= 200 and res.statusCode < 300 fileStream = fs.createWriteStream(filePath) From 94a52333f7c7602ce2a1e94bf0667d80eefd8f1a Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 15:16:06 +0100 Subject: [PATCH 371/709] add sync= off and read_uncommited=true to improve perf --- app/coffee/db.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index a24377e7..764edebe 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -17,6 +17,8 @@ sequelize = new Sequelize( if Settings.mysql.clsi.dialect == "sqlite" logger.log "running PRAGMA journal_mode=WAL;" sequelize.query("PRAGMA journal_mode=WAL;") + sequelize.query("PRAGMA synchronous=OFF;") + sequelize.query("PRAGMA read_uncommitted = true;") module.exports = UrlCache: sequelize.define("UrlCache", { From 92e124063563b863723dba314d97c31d04984bb5 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 15:18:25 +0100 Subject: [PATCH 372/709] added some debugging --- app/coffee/UrlCache.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index b72b78ca..06e60128 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -94,11 +94,13 @@ module.exports = UrlCache = return callback() _findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) -> + console.log("_findUrlDetails") db.UrlCache.find(where: { url: url, project_id: project_id }) .then((urlDetails) -> callback null, urlDetails) .error callback _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) -> + console.log("_updateOrCreateUrlDetails") db.UrlCache.findOrCreate(where: {url: url, project_id: project_id}) .spread( (urlDetails, created) -> @@ -109,11 +111,13 @@ module.exports = UrlCache = .error callback _clearUrlDetails: (project_id, url, callback = (error) ->) -> + console.log("_clearUrlDetails") db.UrlCache.destroy(where: {url: url, project_id: project_id}) .then(() -> callback null) .error callback _findAllUrlsInProject: (project_id, callback = (error, urls) ->) -> + console.log("_findAllUrlsInProject") db.UrlCache.findAll(where: { project_id: project_id }) .then( (urlEntries) -> From 627bed428e2ac7abdcedfcf7a389fa0742099d68 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 16:22:04 +0100 Subject: [PATCH 373/709] added a queue with 1 concurency to db queries --- app/coffee/ProjectPersistenceManager.coffee | 3 + app/coffee/UrlCache.coffee | 68 +++++++++++++-------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 403043fa..17bead29 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -11,6 +11,7 @@ module.exports = ProjectPersistenceManager = EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5 markProjectAsJustAccessed: (project_id, callback = (error) ->) -> + console.log("markProjectAsJustAccessed") db.Project.findOrCreate(where: {project_id: project_id}) .spread( (project, created) -> @@ -53,11 +54,13 @@ module.exports = ProjectPersistenceManager = callback() _clearProjectFromDatabase: (project_id, callback = (error) ->) -> + console.log("_clearProjectFromDatabase") db.Project.destroy(where: {project_id: project_id}) .then(() -> callback()) .error callback _findExpiredProjectIds: (callback = (error, project_ids) ->) -> + console.log("_findExpiredProjectIds") db.Project.findAll(where: ["lastAccessed < ?", new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT)]) .then((projects) -> callback null, projects.map((project) -> project.project_id) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index 06e60128..e5fc81c7 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -6,6 +6,15 @@ fs = require("fs") logger = require "logger-sharelatex" async = require "async" +queue = async.queue((task, cb)-> + console.log("running task") + task(cb) +, 1) + +console.log("hi there queue") +queue.drain = ()-> + console.log('HI all items have been processed') + module.exports = UrlCache = downloadUrlToFile: (project_id, url, destPath, lastModified, callback = (error) ->) -> UrlCache._ensureUrlIsInCache project_id, url, lastModified, (error, pathToCachedUrl) => @@ -51,8 +60,9 @@ module.exports = UrlCache = _doesUrlNeedDownloading: (project_id, url, lastModified, callback = (error, needsDownloading) ->) -> if !lastModified? return callback null, true - + console.log "about to get _findUrlDetails" UrlCache._findUrlDetails project_id, url, (error, urlDetails) -> + console.log error, urlDetails, "_findUrlDetails result" return callback(error) if error? if !urlDetails? or !urlDetails.lastModified? or urlDetails.lastModified.getTime() < lastModified.getTime() return callback null, true @@ -94,36 +104,44 @@ module.exports = UrlCache = return callback() _findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) -> - console.log("_findUrlDetails") - db.UrlCache.find(where: { url: url, project_id: project_id }) - .then((urlDetails) -> callback null, urlDetails) - .error callback + job = (cb)-> + db.UrlCache.find(where: { url: url, project_id: project_id }) + .then((urlDetails) -> cb null, urlDetails) + .error cb + queue.push job, callback _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) -> - console.log("_updateOrCreateUrlDetails") - db.UrlCache.findOrCreate(where: {url: url, project_id: project_id}) - .spread( - (urlDetails, created) -> - urlDetails.updateAttributes(lastModified: lastModified) - .then(() -> callback()) - .error(callback) - ) - .error callback + job = (cb)-> + console.log("_updateOrCreateUrlDetails") + db.UrlCache.findOrCreate(where: {url: url, project_id: project_id}) + .spread( + (urlDetails, created) -> + urlDetails.updateAttributes(lastModified: lastModified) + .then(() -> cb()) + .error(cb) + ) + .error cb + queue.push(job, callback) _clearUrlDetails: (project_id, url, callback = (error) ->) -> - console.log("_clearUrlDetails") - db.UrlCache.destroy(where: {url: url, project_id: project_id}) - .then(() -> callback null) - .error callback + job = (cb)-> + console.log("_clearUrlDetails") + db.UrlCache.destroy(where: {url: url, project_id: project_id}) + .then(() -> cb null) + .error cb + queue.push(job, callback) + _findAllUrlsInProject: (project_id, callback = (error, urls) ->) -> - console.log("_findAllUrlsInProject") - db.UrlCache.findAll(where: { project_id: project_id }) - .then( - (urlEntries) -> - callback null, urlEntries.map((entry) -> entry.url) - ) - .error callback + job = (cb)-> + console.log("_findAllUrlsInProject") + db.UrlCache.findAll(where: { project_id: project_id }) + .then( + (urlEntries) -> + cb null, urlEntries.map((entry) -> entry.url) + ) + .error cb + queue.push(job, callback) From d1ce49d6d7558d8d28433ad6b33fd7206b9892c1 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 16:46:47 +0100 Subject: [PATCH 374/709] add db queue file for global db query queues --- app/coffee/DbQueue.coffee | 13 ++++++ app/coffee/ProjectPersistenceManager.coffee | 46 +++++++++++++-------- app/coffee/UrlCache.coffee | 18 +++----- 3 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 app/coffee/DbQueue.coffee diff --git a/app/coffee/DbQueue.coffee b/app/coffee/DbQueue.coffee new file mode 100644 index 00000000..6d12924c --- /dev/null +++ b/app/coffee/DbQueue.coffee @@ -0,0 +1,13 @@ +async = require "async" + +queue = async.queue((task, cb)-> + console.log("running task") + task(cb) + , 1) + +queue.drain = ()-> + console.log('HI all items have been processed') + +module.exports = + queue: queue + diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 17bead29..427b44b9 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -1,6 +1,7 @@ UrlCache = require "./UrlCache" CompileManager = require "./CompileManager" db = require "./db" +dbQueue = require "./DbQueue" async = require "async" logger = require "logger-sharelatex" oneDay = 24 * 60 * 60 * 1000 @@ -11,15 +12,18 @@ module.exports = ProjectPersistenceManager = EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5 markProjectAsJustAccessed: (project_id, callback = (error) ->) -> - console.log("markProjectAsJustAccessed") - db.Project.findOrCreate(where: {project_id: project_id}) - .spread( - (project, created) -> - project.updateAttributes(lastAccessed: new Date()) - .then(() -> callback()) - .error callback - ) - .error callback + job = (cb)-> + console.log("markProjectAsJustAccessed") + db.Project.findOrCreate(where: {project_id: project_id}) + .spread( + (project, created) -> + project.updateAttributes(lastAccessed: new Date()) + .then(() -> cb()) + .error cb + ) + .error cb + dbQueue.queue.push(job, callback) + clearExpiredProjects: (callback = (error) ->) -> ProjectPersistenceManager._findExpiredProjectIds (error, project_ids) -> @@ -54,16 +58,22 @@ module.exports = ProjectPersistenceManager = callback() _clearProjectFromDatabase: (project_id, callback = (error) ->) -> - console.log("_clearProjectFromDatabase") - db.Project.destroy(where: {project_id: project_id}) - .then(() -> callback()) - .error callback + job = (cb)-> + console.log("_clearProjectFromDatabase") + db.Project.destroy(where: {project_id: project_id}) + .then(() -> callback()) + .error callback + dbQueue.queue.push(job, callback) + _findExpiredProjectIds: (callback = (error, project_ids) ->) -> - console.log("_findExpiredProjectIds") - db.Project.findAll(where: ["lastAccessed < ?", new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT)]) - .then((projects) -> - callback null, projects.map((project) -> project.project_id) - ).error callback + job = (cb)-> + console.log("_findExpiredProjectIds") + db.Project.findAll(where: ["lastAccessed < ?", new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT)]) + .then((projects) -> + callback null, projects.map((project) -> project.project_id) + ).error callback + dbQueue.queue.push(job, callback) + logger.log {EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout" diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index e5fc81c7..73d98411 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -1,4 +1,5 @@ db = require("./db") +dbQueue = require "./DbQueue" UrlFetcher = require("./UrlFetcher") Settings = require("settings-sharelatex") crypto = require("crypto") @@ -6,15 +7,6 @@ fs = require("fs") logger = require "logger-sharelatex" async = require "async" -queue = async.queue((task, cb)-> - console.log("running task") - task(cb) -, 1) - -console.log("hi there queue") -queue.drain = ()-> - console.log('HI all items have been processed') - module.exports = UrlCache = downloadUrlToFile: (project_id, url, destPath, lastModified, callback = (error) ->) -> UrlCache._ensureUrlIsInCache project_id, url, lastModified, (error, pathToCachedUrl) => @@ -108,7 +100,7 @@ module.exports = UrlCache = db.UrlCache.find(where: { url: url, project_id: project_id }) .then((urlDetails) -> cb null, urlDetails) .error cb - queue.push job, callback + dbQueue.queue.push job, callback _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) -> job = (cb)-> @@ -121,7 +113,7 @@ module.exports = UrlCache = .error(cb) ) .error cb - queue.push(job, callback) + dbQueue.queue.push(job, callback) _clearUrlDetails: (project_id, url, callback = (error) ->) -> job = (cb)-> @@ -129,7 +121,7 @@ module.exports = UrlCache = db.UrlCache.destroy(where: {url: url, project_id: project_id}) .then(() -> cb null) .error cb - queue.push(job, callback) + dbQueue.queue.push(job, callback) _findAllUrlsInProject: (project_id, callback = (error, urls) ->) -> @@ -141,7 +133,7 @@ module.exports = UrlCache = cb null, urlEntries.map((entry) -> entry.url) ) .error cb - queue.push(job, callback) + dbQueue.queue.push(job, callback) From 3a9206f1e749ef454b9d69cb8f00ee7548a47ea9 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 17:01:59 +0100 Subject: [PATCH 375/709] =?UTF-8?q?fix=20missing=20cb=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/coffee/ProjectPersistenceManager.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 427b44b9..f41c14cd 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -61,8 +61,8 @@ module.exports = ProjectPersistenceManager = job = (cb)-> console.log("_clearProjectFromDatabase") db.Project.destroy(where: {project_id: project_id}) - .then(() -> callback()) - .error callback + .then(() -> cb()) + .error cb dbQueue.queue.push(job, callback) @@ -71,8 +71,8 @@ module.exports = ProjectPersistenceManager = console.log("_findExpiredProjectIds") db.Project.findAll(where: ["lastAccessed < ?", new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT)]) .then((projects) -> - callback null, projects.map((project) -> project.project_id) - ).error callback + cb null, projects.map((project) -> project.project_id) + ).error cb dbQueue.queue.push(job, callback) From ee518c17551a0a5c3ca13fafb9e7c920a51269df Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 30 Jul 2018 17:37:30 +0100 Subject: [PATCH 376/709] fix expired projects command --- Makefile | 2 +- app/coffee/ProjectPersistenceManager.coffee | 8 ++++++-- app/coffee/db.coffee | 2 ++ docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 6daee1be..b1ce2934 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.8 +# Version: 1.1.9 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index f41c14cd..2c73c614 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -68,11 +68,15 @@ module.exports = ProjectPersistenceManager = _findExpiredProjectIds: (callback = (error, project_ids) ->) -> job = (cb)-> - console.log("_findExpiredProjectIds") - db.Project.findAll(where: ["lastAccessed < ?", new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT)]) + keepProjectsFrom = new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT) + console.log("_findExpiredProjectIds", keepProjectsFrom) + q = {} + q[db.op.gt] = keepProjectsFrom + db.Project.findAll(where:{lastAccessed:q}) .then((projects) -> cb null, projects.map((project) -> project.project_id) ).error cb + dbQueue.queue.push(job, callback) diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index 764edebe..de48dfdf 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -41,6 +41,8 @@ module.exports = ] }) + op: Sequelize.Op + sync: () -> logger.log dbPath:Settings.mysql.clsi.storage, "syncing db schema" sequelize.sync() diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index e8850b93..f6c8a27f 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.8 +# Version: 1.1.9 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index f47658d3..371e6e76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.8 +# Version: 1.1.9 version: "2" From 9ef9a3b780ccbca16fba6afe958a566c0383d0d9 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 31 Jul 2018 14:38:24 +0100 Subject: [PATCH 377/709] make Settings.parallelSqlQueryLimit a config setting --- app/coffee/DbQueue.coffee | 5 +++-- config/settings.defaults.coffee | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/coffee/DbQueue.coffee b/app/coffee/DbQueue.coffee index 6d12924c..79673e4d 100644 --- a/app/coffee/DbQueue.coffee +++ b/app/coffee/DbQueue.coffee @@ -1,13 +1,14 @@ async = require "async" +Settings = require "settings-sharelatex" queue = async.queue((task, cb)-> console.log("running task") task(cb) - , 1) + , Settings.parallelSqlQueryLimit) queue.drain = ()-> console.log('HI all items have been processed') - + module.exports = queue: queue diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index a2bf5930..cfa6bda3 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -36,6 +36,7 @@ module.exports = smokeTest: process.env["SMOKE_TEST"] or false project_cache_length_ms: 1000 * 60 * 60 * 24 parallelFileDownloads:1 + parallelSqlQueryLimit:1 filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"] From 3e3468d9e9efee15be1768de99609b519e1810f0 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 1 Aug 2018 13:59:09 +0100 Subject: [PATCH 378/709] reduce logging --- app/coffee/DbQueue.coffee | 1 - app/coffee/DockerRunner.coffee | 12 +++++++----- app/coffee/ProjectPersistenceManager.coffee | 3 --- app/coffee/UrlCache.coffee | 5 ----- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/app/coffee/DbQueue.coffee b/app/coffee/DbQueue.coffee index 79673e4d..0e025906 100644 --- a/app/coffee/DbQueue.coffee +++ b/app/coffee/DbQueue.coffee @@ -2,7 +2,6 @@ async = require "async" Settings = require "settings-sharelatex" queue = async.queue((task, cb)-> - console.log("running task") task(cb) , Settings.parallelSqlQueryLimit) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 69a3df97..589a2e03 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -46,7 +46,9 @@ module.exports = DockerRunner = fingerprint = DockerRunner._fingerprintContainer(options) options.name = name = "project-#{project_id}-#{fingerprint}" - logger.log project_id: project_id, options: options, "running docker container" + logOptions = _.clone(options) + logOptions.HostConfig.SecurityOpt = "secomp used, removed in logging" + logger.log project_id: project_id, options:logOptions, "running docker container" DockerRunner._runAndWaitForContainer options, volumes, timeout, (error, output) -> if error?.message?.match("HTTP code is 500") logger.log err: error, project_id: project_id, "error running container so destroying and retrying" @@ -144,14 +146,14 @@ module.exports = DockerRunner = "Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}] "CapDrop": "ALL" "SecurityOpt": ["no-new-privileges"] - - if Settings.clsi.docker.seccomp_profile? - options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}" + if Settings.path?.synctexBinHostPath? options["HostConfig"]["Binds"].push("#{Settings.path.synctexBinHostPath}:/opt/synctex:ro") - logger.log options:options, "options for running docker container" + if Settings.clsi.docker.seccomp_profile? + options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}" + return options _fingerprintContainer: (containerOptions) -> diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 2c73c614..944ea708 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -13,7 +13,6 @@ module.exports = ProjectPersistenceManager = markProjectAsJustAccessed: (project_id, callback = (error) ->) -> job = (cb)-> - console.log("markProjectAsJustAccessed") db.Project.findOrCreate(where: {project_id: project_id}) .spread( (project, created) -> @@ -59,7 +58,6 @@ module.exports = ProjectPersistenceManager = _clearProjectFromDatabase: (project_id, callback = (error) ->) -> job = (cb)-> - console.log("_clearProjectFromDatabase") db.Project.destroy(where: {project_id: project_id}) .then(() -> cb()) .error cb @@ -69,7 +67,6 @@ module.exports = ProjectPersistenceManager = _findExpiredProjectIds: (callback = (error, project_ids) ->) -> job = (cb)-> keepProjectsFrom = new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT) - console.log("_findExpiredProjectIds", keepProjectsFrom) q = {} q[db.op.gt] = keepProjectsFrom db.Project.findAll(where:{lastAccessed:q}) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index 73d98411..d44479a1 100644 --- a/app/coffee/UrlCache.coffee +++ b/app/coffee/UrlCache.coffee @@ -52,9 +52,7 @@ module.exports = UrlCache = _doesUrlNeedDownloading: (project_id, url, lastModified, callback = (error, needsDownloading) ->) -> if !lastModified? return callback null, true - console.log "about to get _findUrlDetails" UrlCache._findUrlDetails project_id, url, (error, urlDetails) -> - console.log error, urlDetails, "_findUrlDetails result" return callback(error) if error? if !urlDetails? or !urlDetails.lastModified? or urlDetails.lastModified.getTime() < lastModified.getTime() return callback null, true @@ -104,7 +102,6 @@ module.exports = UrlCache = _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) -> job = (cb)-> - console.log("_updateOrCreateUrlDetails") db.UrlCache.findOrCreate(where: {url: url, project_id: project_id}) .spread( (urlDetails, created) -> @@ -117,7 +114,6 @@ module.exports = UrlCache = _clearUrlDetails: (project_id, url, callback = (error) ->) -> job = (cb)-> - console.log("_clearUrlDetails") db.UrlCache.destroy(where: {url: url, project_id: project_id}) .then(() -> cb null) .error cb @@ -126,7 +122,6 @@ module.exports = UrlCache = _findAllUrlsInProject: (project_id, callback = (error, urls) ->) -> job = (cb)-> - console.log("_findAllUrlsInProject") db.UrlCache.findAll(where: { project_id: project_id }) .then( (urlEntries) -> From 3890cdec373fa52a39990e85870821ae8d6c1b68 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 1 Aug 2018 14:10:22 +0100 Subject: [PATCH 379/709] null check host options --- app/coffee/DockerRunner.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 589a2e03..1e2612cc 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -47,7 +47,7 @@ module.exports = DockerRunner = options.name = name = "project-#{project_id}-#{fingerprint}" logOptions = _.clone(options) - logOptions.HostConfig.SecurityOpt = "secomp used, removed in logging" + logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" logger.log project_id: project_id, options:logOptions, "running docker container" DockerRunner._runAndWaitForContainer options, volumes, timeout, (error, output) -> if error?.message?.match("HTTP code is 500") From 95b2e8caae5483faaf52aebeebc847b05808bdb3 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 1 Aug 2018 14:32:17 +0100 Subject: [PATCH 380/709] comment out erroring log for moment --- app/coffee/DockerRunner.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 1e2612cc..d529b2de 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -46,9 +46,9 @@ module.exports = DockerRunner = fingerprint = DockerRunner._fingerprintContainer(options) options.name = name = "project-#{project_id}-#{fingerprint}" - logOptions = _.clone(options) - logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" - logger.log project_id: project_id, options:logOptions, "running docker container" + # logOptions = _.clone(options) + # logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" + logger.log project_id: project_id, "running docker container" DockerRunner._runAndWaitForContainer options, volumes, timeout, (error, output) -> if error?.message?.match("HTTP code is 500") logger.log err: error, project_id: project_id, "error running container so destroying and retrying" From 9f7922983584808035acdb9b0436295debfa4fd2 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 3 Aug 2018 15:33:53 +0100 Subject: [PATCH 381/709] Read sentry dsn from env --- config/settings.defaults.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index cfa6bda3..6f4f160e 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -39,6 +39,8 @@ module.exports = parallelSqlQueryLimit:1 filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"] + sentry: + dsn: process.env['SENTRY_DSN'] if process.env["DOCKER_RUNNER"] From 95e052d05910680aeeca2521b65b089122c4e249 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 13 Aug 2018 12:27:13 +0100 Subject: [PATCH 382/709] Put a guard on sentry dsn --- config/settings.defaults.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 6f4f160e..19fcb326 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -39,6 +39,7 @@ module.exports = parallelSqlQueryLimit:1 filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"] +if process.env['SENTRY_DSN'] sentry: dsn: process.env['SENTRY_DSN'] From 382f30f810688f2de073658b7026b924d2c1c74e Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 13 Aug 2018 17:36:53 +0100 Subject: [PATCH 383/709] Revert "Put a guard on sentry dsn" This reverts commit 95e052d05910680aeeca2521b65b089122c4e249. --- config/settings.defaults.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 19fcb326..6f4f160e 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -39,7 +39,6 @@ module.exports = parallelSqlQueryLimit:1 filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"] -if process.env['SENTRY_DSN'] sentry: dsn: process.env['SENTRY_DSN'] From eec0529ef7575248b09baee58398e8c963794520 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 14 Aug 2018 15:17:56 +0100 Subject: [PATCH 384/709] put FILESTORE_PARALLEL_FILE_DOWNLOADS and FILESTORE_PARALLEL_SQL_QUERY_LIMIT into env vars --- config/settings.defaults.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index cfa6bda3..6dae3fec 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -35,8 +35,8 @@ module.exports = smokeTest: process.env["SMOKE_TEST"] or false project_cache_length_ms: 1000 * 60 * 60 * 24 - parallelFileDownloads:1 - parallelSqlQueryLimit:1 + parallelFileDownloads: process.env["FILESTORE_PARALLEL_FILE_DOWNLOADS"] or 1 + parallelSqlQueryLimit: process.env["FILESTORE_PARALLEL_SQL_QUERY_LIMIT"] or 1 filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"] From 988f177f79ec47ee862d23e81605cda2d99b0315 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Sun, 19 Aug 2018 11:38:27 +0100 Subject: [PATCH 385/709] added loads of debugging --- app/coffee/LockManager.coffee | 7 +++++-- app/coffee/ProjectPersistenceManager.coffee | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/coffee/LockManager.coffee b/app/coffee/LockManager.coffee index 5d6fa462..2877d5f9 100644 --- a/app/coffee/LockManager.coffee +++ b/app/coffee/LockManager.coffee @@ -2,7 +2,8 @@ Settings = require('settings-sharelatex') logger = require "logger-sharelatex" Lockfile = require('lockfile') # from https://github.com/npm/lockfile Errors = require "./Errors" - +fs = require("fs") +Path = require("path") module.exports = LockManager = LOCK_TEST_INTERVAL: 1000 # 50ms between each test of the lock MAX_LOCK_WAIT_TIME: 15000 # 10s maximum time to spend trying to get the lock @@ -15,7 +16,9 @@ module.exports = LockManager = stale: @LOCK_STALE Lockfile.lock path, lockOpts, (error) -> return callback new Errors.AlreadyCompilingError("compile in progress") if error?.code is 'EEXIST' - return callback(error) if error? + if error? + logger.err error:error, path:path, statLock:fs.lstatSync(path), statDir: fs.lstatSync(path.dirname(path)), "unable to get lock" + return callback(error) runner (error1, args...) -> Lockfile.unlock path, (error2) -> error = error1 or error2 diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 944ea708..2565af39 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -51,12 +51,16 @@ module.exports = ProjectPersistenceManager = clearProjectFromCache: (project_id, callback = (error) ->) -> logger.log project_id: project_id, "clearing project from cache" UrlCache.clearProject project_id, (error) -> - return callback(error) if error? + if error? + logger.err error:error, project_id: project_id, "error clearing project from cache" + return callback(error) ProjectPersistenceManager._clearProjectFromDatabase project_id, (error) -> - return callback(error) if error? - callback() + if error? + logger.err error:error, project_id:project_id, "error clearing project from database" + callback(error) _clearProjectFromDatabase: (project_id, callback = (error) ->) -> + logger.log project_id:project_id, "clearing project from database" job = (cb)-> db.Project.destroy(where: {project_id: project_id}) .then(() -> cb()) From 1990f20dc06c64b117de2f5100e01ea3682eb99b Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 20 Aug 2018 10:12:32 +0100 Subject: [PATCH 386/709] improve error reporting --- app/coffee/CompileManager.coffee | 3 ++- app/coffee/DockerRunner.coffee | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 807b8c44..daa90217 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -290,7 +290,8 @@ module.exports = CompileManager = return callback(error) if error? 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" + #call it node_err so sentry doesn't use random path error as unique id so it can't be ignored + logger.err node_err:err, command:command, directory:directory, project_id:project_id, user_id:user_id, "error reading word count output" return callback(err) results = CompileManager._parseWordcountFromOutput(stdout) logger.log project_id:project_id, user_id:user_id, wordcount: results, "word count results" diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index d529b2de..42c9f9a8 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -214,7 +214,7 @@ module.exports = DockerRunner = if error?.statusCode == 404 createAndStartContainer() else if error? - logger.err {container_name: name}, "unable to inspect container to start" + logger.err {container_name: name, error:error}, "unable to inspect container to start" return callback(error) else startExistingContainer() From 0f179a7c7cbd9553202588da199ea7d6d5fcdf05 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 21 Aug 2018 12:02:12 +0100 Subject: [PATCH 387/709] add log on exited error code --- app/coffee/DockerRunner.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 42c9f9a8..d1337d64 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -107,6 +107,7 @@ module.exports = DockerRunner = if exitCode is 1 # exit status from chktex err = DockerRunner.ERR_EXITED err.code = exitCode + logger.err err:err, exitCode:exitCode, options:options, "docker container has exited" return callback(err) containerReturned = true callbackIfFinished() From 834eeffda4fe4e0e7ba2a0e0ce3833be3b30d886 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 21 Aug 2018 18:56:53 +0100 Subject: [PATCH 388/709] add time secomp --- seccomp/clsi-profile.json | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/seccomp/clsi-profile.json b/seccomp/clsi-profile.json index cab7a445..34fd2520 100644 --- a/seccomp/clsi-profile.json +++ b/seccomp/clsi-profile.json @@ -31,6 +31,21 @@ "action": "SCMP_ACT_ALLOW", "args": [] }, + { + "name": "clock_getres", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_gettime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_nanosleep", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, { "name": "clone", "action": "SCMP_ACT_ALLOW", @@ -668,6 +683,31 @@ "action": "SCMP_ACT_ALLOW", "args": [] }, + { + "name": "timer_create", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_delete", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_getoverrun", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_gettime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_settime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, { "name": "times", "action": "SCMP_ACT_ALLOW", From 171ad0329da77c54f9900026f3ff3e85c1ec917c Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 22 Aug 2018 18:21:15 +0100 Subject: [PATCH 389/709] fix sql query checking last access time --- app/coffee/ProjectPersistenceManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 2565af39..4ea02bf7 100644 --- a/app/coffee/ProjectPersistenceManager.coffee +++ b/app/coffee/ProjectPersistenceManager.coffee @@ -72,7 +72,7 @@ module.exports = ProjectPersistenceManager = job = (cb)-> keepProjectsFrom = new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT) q = {} - q[db.op.gt] = keepProjectsFrom + q[db.op.lt] = keepProjectsFrom db.Project.findAll(where:{lastAccessed:q}) .then((projects) -> cb null, projects.map((project) -> project.project_id) From e4d28addf9d617cd569bf86c707b731676af0ebe Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 22 Aug 2018 21:32:19 +0100 Subject: [PATCH 390/709] change sync to async for lockfile debugging --- app/coffee/LockManager.coffee | 18 ++++++++++-------- test/unit/coffee/LockManagerTests.coffee | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/coffee/LockManager.coffee b/app/coffee/LockManager.coffee index 2877d5f9..069e26b2 100644 --- a/app/coffee/LockManager.coffee +++ b/app/coffee/LockManager.coffee @@ -16,11 +16,13 @@ module.exports = LockManager = stale: @LOCK_STALE Lockfile.lock path, lockOpts, (error) -> return callback new Errors.AlreadyCompilingError("compile in progress") if error?.code is 'EEXIST' - if error? - logger.err error:error, path:path, statLock:fs.lstatSync(path), statDir: fs.lstatSync(path.dirname(path)), "unable to get lock" - return callback(error) - runner (error1, args...) -> - Lockfile.unlock path, (error2) -> - error = error1 or error2 - return callback(error) if error? - callback(null, args...) + fs.lstat path, (err, statLock)-> + fs.lstat Path.dirname(path), (err, statDir)-> + if error? + logger.err error:error, path:path, statLock:statLock, statDir: statDir, "unable to get lock" + return callback(error) + runner (error1, args...) -> + Lockfile.unlock path, (error2) -> + error = error1 or error2 + return callback(error) if error? + callback(null, args...) diff --git a/test/unit/coffee/LockManagerTests.coffee b/test/unit/coffee/LockManagerTests.coffee index 2d0c95af..ab9ce938 100644 --- a/test/unit/coffee/LockManagerTests.coffee +++ b/test/unit/coffee/LockManagerTests.coffee @@ -10,6 +10,8 @@ describe "DockerLockManager", -> @LockManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": {} "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + "fs": + lstat:sinon.stub().callsArgWith(1) "lockfile": @Lockfile = {} @lockFile = "/local/compile/directory/.project-lock" From 7b773474d97931dd1e331bd6dfb17c75dfff9ee6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 22 Aug 2018 23:54:40 +0100 Subject: [PATCH 391/709] improve error reporting --- app/coffee/LockManager.coffee | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/coffee/LockManager.coffee b/app/coffee/LockManager.coffee index 069e26b2..2a8777fd 100644 --- a/app/coffee/LockManager.coffee +++ b/app/coffee/LockManager.coffee @@ -16,11 +16,13 @@ module.exports = LockManager = stale: @LOCK_STALE Lockfile.lock path, lockOpts, (error) -> return callback new Errors.AlreadyCompilingError("compile in progress") if error?.code is 'EEXIST' - fs.lstat path, (err, statLock)-> - fs.lstat Path.dirname(path), (err, statDir)-> - if error? - logger.err error:error, path:path, statLock:statLock, statDir: statDir, "unable to get lock" - return callback(error) + if error? + fs.lstat path, (statLockErr, statLock)-> + fs.lstat Path.dirname(path), (statDirErr, statDir)-> + fs.readdir Path.dirname(path), (readdirErr, readdirDir)-> + logger.err error:error, path:path, statLock:statLock, statLockErr:statLockErr, statDir:statDir, statDirErr: statDirErr, readdirErr:readdirErr, readdirDir:readdirDir, "unable to get lock" + return callback(error) + else runner (error1, args...) -> Lockfile.unlock path, (error2) -> error = error1 or error2 From 05ddbd3a18ae882fbc5a0ec5db5dd0d6e89f492a Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 23 Aug 2018 00:10:06 +0100 Subject: [PATCH 392/709] try changing bin to be owned by node --- entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/entrypoint.sh b/entrypoint.sh index 32776fa5..cb2580cb 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -17,6 +17,7 @@ chown -R node:node /app/bin/synctex mkdir -p /app/test/acceptance/fixtures/tmp/ chown -R node:node /app +chown -R node:node /app/bin ./bin/install_texlive_gce.sh exec runuser -u node "$@" \ No newline at end of file From 5074442702d34d3112ee6504c5aaa3c284294507 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 23 Aug 2018 00:12:05 +0100 Subject: [PATCH 393/709] fix unit tests --- app/coffee/LockManager.coffee | 23 ++++++++++++----------- test/unit/coffee/LockManagerTests.coffee | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/coffee/LockManager.coffee b/app/coffee/LockManager.coffee index 2a8777fd..afa3cca9 100644 --- a/app/coffee/LockManager.coffee +++ b/app/coffee/LockManager.coffee @@ -15,16 +15,17 @@ module.exports = LockManager = pollPeriod: @LOCK_TEST_INTERVAL stale: @LOCK_STALE Lockfile.lock path, lockOpts, (error) -> - return callback new Errors.AlreadyCompilingError("compile in progress") if error?.code is 'EEXIST' - if error? - fs.lstat path, (statLockErr, statLock)-> - fs.lstat Path.dirname(path), (statDirErr, statDir)-> - fs.readdir Path.dirname(path), (readdirErr, readdirDir)-> + if error?.code is 'EEXIST' + return callback new Errors.AlreadyCompilingError("compile in progress") + else if error? + fs.lstat path, (statLockErr, statLock)-> + fs.lstat Path.dirname(path), (statDirErr, statDir)-> + fs.readdir Path.dirname(path), (readdirErr, readdirDir)-> logger.err error:error, path:path, statLock:statLock, statLockErr:statLockErr, statDir:statDir, statDirErr: statDirErr, readdirErr:readdirErr, readdirDir:readdirDir, "unable to get lock" return callback(error) - else - runner (error1, args...) -> - Lockfile.unlock path, (error2) -> - error = error1 or error2 - return callback(error) if error? - callback(null, args...) + else + runner (error1, args...) -> + Lockfile.unlock path, (error2) -> + error = error1 or error2 + return callback(error) if error? + callback(null, args...) diff --git a/test/unit/coffee/LockManagerTests.coffee b/test/unit/coffee/LockManagerTests.coffee index ab9ce938..9dd1d46c 100644 --- a/test/unit/coffee/LockManagerTests.coffee +++ b/test/unit/coffee/LockManagerTests.coffee @@ -9,9 +9,10 @@ describe "DockerLockManager", -> beforeEach -> @LockManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err:-> } "fs": lstat:sinon.stub().callsArgWith(1) + readdir: sinon.stub().callsArgWith(1) "lockfile": @Lockfile = {} @lockFile = "/local/compile/directory/.project-lock" From b4107b7391d2cd00da152a5253006d2873b85034 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 23 Aug 2018 08:34:18 +0100 Subject: [PATCH 394/709] fse.ensureDir when running synctex and wordcount --- app/coffee/CompileController.coffee | 2 - app/coffee/CompileManager.coffee | 59 +++++++++++++++++------------ 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 91137f6d..5d1ee2e7 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -82,7 +82,6 @@ module.exports = CompileController = column = parseInt(req.query.column, 10) project_id = req.params.project_id user_id = req.params.user_id - CompileManager.syncFromCode project_id, user_id, file, line, column, (error, pdfPositions) -> return next(error) if error? res.send JSON.stringify { @@ -95,7 +94,6 @@ module.exports = CompileController = v = parseFloat(req.query.v) project_id = req.params.project_id user_id = req.params.user_id - CompileManager.syncFromPdf project_id, user_id, page, h, v, (error, codePositions) -> return next(error) if error? res.send JSON.stringify { diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index daa90217..f40a98b1 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -203,14 +203,18 @@ module.exports = CompileManager = compileDir = getCompileDir(project_id, user_id) synctex_path = "#{base_dir}/output.pdf" command = ["code", synctex_path, file_path, line, column] - CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> - return callback(error) if error? - if stdout.toLowerCase().indexOf("warning") == -1 - logType = "log" - else - logType = "err" - logger[logType] project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, command:command, stdout: stdout, "synctex code output" - callback null, CompileManager._parseSynctexFromCodeOutput(stdout) + fse.ensureDir compileDir, (error) -> + if error? + logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync from code" + return callback(error) + CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> + return callback(error) if error? + if stdout.toLowerCase().indexOf("warning") == -1 + logType = "log" + else + logType = "err" + logger[logType] project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, command:command, stdout: stdout, "synctex code output" + callback null, CompileManager._parseSynctexFromCodeOutput(stdout) syncFromPdf: (project_id, user_id, page, h, v, callback = (error, filePositions) ->) -> compileName = getCompileName(project_id, user_id) @@ -218,10 +222,14 @@ module.exports = CompileManager = base_dir = Settings.path.synctexBaseDir(compileName) synctex_path = "#{base_dir}/output.pdf" command = ["pdf", synctex_path, page, h, v] - CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> - return callback(error) if error? - logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" - callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) + fse.ensureDir compileDir, (error) -> + if error? + logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync to code" + return callback(error) + CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> + return callback(error) if error? + logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" + callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) _checkFileExists: (path, callback = (error) ->) -> synctexDir = Path.dirname(path) @@ -282,20 +290,23 @@ module.exports = CompileManager = 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", '-nocol', '-inc', file_path, "-out=" + file_path + ".wc"] - directory = getCompileDir(project_id, user_id) + compileDir = getCompileDir(project_id, user_id) timeout = 10 * 1000 compileName = getCompileName(project_id, user_id) - - CommandRunner.run compileName, command, directory, image, timeout, {}, (error) -> - return callback(error) if error? - fs.readFile directory + "/" + file_name + ".wc", "utf-8", (err, stdout) -> - if err? - #call it node_err so sentry doesn't use random path error as unique id so it can't be ignored - logger.err node_err:err, command:command, directory:directory, project_id:project_id, user_id:user_id, "error reading word count output" - return callback(err) - results = CompileManager._parseWordcountFromOutput(stdout) - logger.log project_id:project_id, user_id:user_id, wordcount: results, "word count results" - callback null, results + fse.ensureDir compileDir, (error) -> + if error? + logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync from code" + return callback(error) + CommandRunner.run compileName, command, compileDir, image, timeout, {}, (error) -> + return callback(error) if error? + fs.readFile compileDir + "/" + file_name + ".wc", "utf-8", (err, stdout) -> + if err? + #call it node_err so sentry doesn't use random path error as unique id so it can't be ignored + logger.err node_err:err, command:command, compileDir:compileDir, project_id:project_id, user_id:user_id, "error reading word count output" + return callback(err) + 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 = { From 607bb74ffa070793bc900a1dbe9b72487ea3b17e Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 23 Aug 2018 11:16:28 +0100 Subject: [PATCH 395/709] reduce log level --- app/coffee/DockerRunner.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index d1337d64..e71a9333 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -107,9 +107,10 @@ module.exports = DockerRunner = if exitCode is 1 # exit status from chktex err = DockerRunner.ERR_EXITED err.code = exitCode - logger.err err:err, exitCode:exitCode, options:options, "docker container has exited" return callback(err) containerReturned = true + options.SecurityOpt = null #small log line + logger.log err:err, exitCode:exitCode, options:options, "docker container has exited" callbackIfFinished() _getContainerOptions: (command, image, volumes, timeout, environment) -> From 6299832a13ed56aa3cc56e799714782d0180df9b Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 23 Aug 2018 11:18:05 +0100 Subject: [PATCH 396/709] don't error on a bad synctex call --- app/coffee/CompileManager.coffee | 6 +----- app/coffee/DockerRunner.coffee | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index f40a98b1..0436a898 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -209,11 +209,7 @@ module.exports = CompileManager = return callback(error) CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> return callback(error) if error? - if stdout.toLowerCase().indexOf("warning") == -1 - logType = "log" - else - logType = "err" - logger[logType] project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, command:command, stdout: stdout, "synctex code output" + logger.log project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, command:command, stdout: stdout, "synctex code output" callback null, CompileManager._parseSynctexFromCodeOutput(stdout) syncFromPdf: (project_id, user_id, page, h, v, callback = (error, filePositions) ->) -> diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index e71a9333..3c2ed9c5 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -109,7 +109,7 @@ module.exports = DockerRunner = err.code = exitCode return callback(err) containerReturned = true - options.SecurityOpt = null #small log line + options?.HostConfig?.SecurityOpt = null #small log line logger.log err:err, exitCode:exitCode, options:options, "docker container has exited" callbackIfFinished() From 00ebc872307f07fb317bcd03b56b4a1b5dfacad9 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 11 Sep 2018 09:44:22 +0100 Subject: [PATCH 397/709] cache pdf files generated by epstopdf --- app/coffee/ResourceWriter.coffee | 2 ++ test/unit/coffee/ResourceWriterTests.coffee | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 0b6aef5b..85df35da 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -80,6 +80,8 @@ module.exports = ResourceWriter = should_delete = false if path.match(/^output-.*/) # Tikz cached figures should_delete = false + if path.match(/-eps-converted-to\.pdf$/) # Epstopdf generated files + should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" should_delete = true if path == "output.tex" # created by TikzManager if present in output files diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index fbc8916c..66a8a36d 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -134,6 +134,9 @@ describe "ResourceWriter", -> type: "aux" }, { path: "cache/_chunk1" + },{ + path: "figures/image-eps-converted-to.pdf" + type: "pdf" }] @resources = "mock-resources" @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @@ -165,6 +168,11 @@ describe "ResourceWriter", -> .calledWith(path.join(@basePath, "cache/_chunk1")) .should.equal false + it "should not delete the epstopdf converted files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "figures/image-eps-converted-to.pdf")) + .should.equal false + it "should call the callback", -> @callback.called.should.equal true From d3bb863d0a989d0df74e6a613c701314733b552f Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 11 Sep 2018 09:51:20 +0100 Subject: [PATCH 398/709] improve synctex logging --- app/coffee/CompileManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 0436a898..64f2665c 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -251,7 +251,7 @@ module.exports = CompileManager = compileName = getCompileName(project_id, user_id) CommandRunner.run compileName, command, directory, Settings.clsi.docker.image, timeout, {}, (error, output) -> if error? - logger.err err:error, command:command, "error running synctex" + logger.err err:error, command:command, project_id:project_id, user_id:user_id, "error running synctex" return callback(error) callback(null, output.stdout) From 5f9fb85613857dbfb8db6ba6524156788378b9bd Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 11 Sep 2018 09:54:44 +0100 Subject: [PATCH 399/709] bump wordcount timeouts, taken from 82b996b145196711e439d7d7045f53498c1afa1a --- app/coffee/CompileManager.coffee | 2 +- test/unit/coffee/CompileManagerTests.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 64f2665c..893d4611 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -247,7 +247,7 @@ module.exports = CompileManager = command.unshift("/opt/synctex") directory = getCompileDir(project_id, user_id) - timeout = 10 * 1000 + timeout = 60 * 1000 # increased to allow for large projects compileName = getCompileName(project_id, user_id) CommandRunner.run compileName, command, directory, Settings.clsi.docker.image, timeout, {}, (error, output) -> if error? diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 93b7f11b..26524fb7 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -319,7 +319,7 @@ describe "CompileManager", -> @callback = sinon.stub() @project_id - @timeout = 10 * 1000 + @timeout = 60 * 1000 @file_name = "main.tex" @Settings.path.compilesDir = "/local/compile/directory" @image = "example.com/image" From b07b7a84be650575ec8fdab1f7787a1d04126d19 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 11 Sep 2018 10:21:37 +0100 Subject: [PATCH 400/709] fix unit tests --- app/coffee/CompileManager.coffee | 5 +++-- test/unit/coffee/CompileManagerTests.coffee | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 9c28535b..eff20ebd 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -282,12 +282,13 @@ module.exports = CompileManager = } return results + 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", '-nocol', '-inc', file_path, "-out=" + file_path + ".wc"] - directory = getCompileDir(project_id, user_id) - timeout = 60 * 1000 # increased to allow for large projects + compileDir = getCompileDir(project_id, user_id) + timeout = 60 * 1000 compileName = getCompileName(project_id, user_id) fse.ensureDir compileDir, (error) -> if error? diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 26524fb7..608a3e55 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -269,7 +269,7 @@ describe "CompileManager", -> ['/opt/synctex', 'code', synctex_path, file_path, @line, @column], "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}", @Settings.clsi.docker.image, - 10000, + 60000, {} ).should.equal true @@ -300,7 +300,7 @@ describe "CompileManager", -> ['/opt/synctex', "pdf", synctex_path, @page, @h, @v], "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}", @Settings.clsi.docker.image, - 10000, + 60000, {}).should.equal true it "should call the callback with the parsed output", -> From a781c7f60085f0546b3f9e253c1643c0e2501ff6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 11 Sep 2018 11:34:25 +0100 Subject: [PATCH 401/709] change timeout test latex code --- test/acceptance/coffee/TimeoutTests.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index 877223a9..c3acd8f9 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -8,13 +8,14 @@ describe "Timed out compile", -> before (done) -> @request = options: - timeout: 1 #seconds + timeout: 10 #seconds resources: [ path: "main.tex" content: ''' \\documentclass{article} \\begin{document} - \\input{|"/bin/bash -c ':(){ :|:& };:'"} + \\def\\x{Hello!\\par\\x} + \\x \\end{document} ''' ] From 7c4c8a9e444cea7199fa238ed0284270b94396e6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 14 Sep 2018 10:26:40 +0100 Subject: [PATCH 402/709] remove debugging get settings function --- app.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app.coffee b/app.coffee index 8b5c779d..367174f3 100644 --- a/app.coffee +++ b/app.coffee @@ -143,11 +143,6 @@ app.get "/health_check", (req, res)-> app.get "/smoke_test_force", (req, res)-> smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res) - -#TODO delete this -app.get "/settings", (req, res)-> - res.json(Settings) - profiler = require "v8-profiler" app.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") From 9ce7bfa8abc7b5badfdf750dcd942fd2fe615dfc Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 4 Oct 2018 16:38:41 +0100 Subject: [PATCH 403/709] extend caching for tikz, minted and markdown files --- app/coffee/ResourceWriter.coffee | 8 ++- test/unit/coffee/ResourceWriterTests.coffee | 56 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 12855723..f3a4bd07 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -78,7 +78,13 @@ module.exports = ResourceWriter = should_delete = true if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache should_delete = false - if path.match(/^output-.*/) # Tikz cached figures + if path.match(/^output-.*/) # Tikz cached figures (default case) + should_delete = false + if path.match(/\.(pdf|dpth|md5)$/) # Tikz cached figures (by extension) + should_delete = false + if path.match(/\.(pygtex|pygstyle)$/) or path.match(/(^|\/)_minted-[^\/]+\//) # minted files/directory + should_delete = false + if path.match(/\.md\.tex$/) or path.match(/(^|\/)_markdown_[^\/]+\//) # markdown files/directory should_delete = false if path.match(/-eps-converted-to\.pdf$/) # Epstopdf generated files should_delete = false diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index 66a8a36d..4a88226f 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -137,6 +137,27 @@ describe "ResourceWriter", -> },{ path: "figures/image-eps-converted-to.pdf" type: "pdf" + },{ + path: "foo/main-figure0.md5" + type: "md5" + }, { + path: "foo/main-figure0.dpth" + type: "dpth" + }, { + path: "foo/main-figure0.pdf" + type: "pdf" + }, { + path: "_minted-main/default-pyg-prefix.pygstyle" + type: "pygstyle" + }, { + path: "_minted-main/default.pygstyle" + type: "pygstyle" + }, { + path: "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex" + type: "pygtex" + }, { + path: "_markdown_main/30893013dec5d869a415610079774c2f.md.tex" + type: "tex" }] @resources = "mock-resources" @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) @@ -173,6 +194,41 @@ describe "ResourceWriter", -> .calledWith(path.join(@basePath, "figures/image-eps-converted-to.pdf")) .should.equal false + it "should not delete the tikz md5 files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "foo/main-figure0.md5")) + .should.equal false + + it "should not delete the tikz dpth files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "foo/main-figure0.dpth")) + .should.equal false + + it "should not delete the tikz pdf files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "foo/main-figure0.pdf")) + .should.equal false + + it "should not delete the minted pygstyle files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "_minted-main/default-pyg-prefix.pygstyle")) + .should.equal false + + it "should not delete the minted default pygstyle files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "_minted-main/default.pygstyle")) + .should.equal false + + it "should not delete the minted default pygtex files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex")) + .should.equal false + + it "should not delete the markdown md.tex files", -> + @ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(@basePath, "_markdown_main/30893013dec5d869a415610079774c2f.md.tex")) + .should.equal false + it "should call the callback", -> @callback.called.should.equal true From 49ddcee0c695a3f52dadb7c3a29f1f26784ad0d9 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 10 Oct 2018 16:13:20 +0100 Subject: [PATCH 404/709] use TikzManager to create main file for pstool package --- app/coffee/CompileManager.coffee | 4 ++-- app/coffee/TikzManager.coffee | 12 +++++++----- test/unit/coffee/TikzManager.coffee | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index eff20ebd..0a98e87d 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -58,9 +58,9 @@ module.exports = CompileManager = callback() createTikzFileIfRequired = (callback) -> - TikzManager.checkMainFile compileDir, request.rootResourcePath, resourceList, (error, usesTikzExternalize) -> + TikzManager.checkMainFile compileDir, request.rootResourcePath, resourceList, (error, needsMainFile) -> return callback(error) if error? - if usesTikzExternalize + if needsMainFile TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback else callback() diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index 7605b0d2..60cb1e34 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -4,26 +4,28 @@ ResourceWriter = require "./ResourceWriter" SafeReader = require "./SafeReader" logger = require "logger-sharelatex" -# for \tikzexternalize to work the main file needs to match the +# for \tikzexternalize or pstool to work the main file needs to match the # jobname. Since we set the -jobname to output, we have to create a # copy of the main file as 'output.tex'. module.exports = TikzManager = - checkMainFile: (compileDir, mainFile, resources, callback = (error, usesTikzExternalize) ->) -> + checkMainFile: (compileDir, mainFile, resources, callback = (error, needsMainFile) ->) -> # if there's already an output.tex file, we don't want to touch it for resource in resources if resource.path is "output.tex" logger.log compileDir: compileDir, mainFile: mainFile, "output.tex already in resources" return callback(null, false) - # if there's no output.tex, see if we are using tikz/pgf in the main file + # if there's no output.tex, see if we are using tikz/pgf or pstool in the main file ResourceWriter.checkPath compileDir, mainFile, (error, path) -> return callback(error) if error? SafeReader.readFile path, 65536, "utf8", (error, content) -> return callback(error) if error? usesTikzExternalize = content?.indexOf("\\tikzexternalize") >= 0 - logger.log compileDir: compileDir, mainFile: mainFile, usesTikzExternalize:usesTikzExternalize, "checked for tikzexternalize" - callback null, usesTikzExternalize + usesPsTool = content.indexOf("{pstool}") >= 0 + logger.log compileDir: compileDir, mainFile: mainFile, usesTikzExternalize:usesTikzExternalize, usesPsTool: usesPsTool, "checked for packages needing main file as output.tex" + needsMainFile = (usesTikzExternalize || usesPsTool) + callback null, needsMainFile injectOutputFile: (compileDir, mainFile, callback = (error) ->) -> ResourceWriter.checkPath compileDir, mainFile, (error, path) -> diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.coffee index 5a3ec5ce..69968aa5 100644 --- a/test/unit/coffee/TikzManager.coffee +++ b/test/unit/coffee/TikzManager.coffee @@ -65,6 +65,22 @@ describe 'TikzManager', -> @callback.calledWithExactly(null, false) .should.equal true + describe "and the main file contains \\usepackage{pstool}", -> + beforeEach -> + @SafeReader.readFile = sinon.stub() + .withArgs("#{@compileDir}/#{@mainFile}") + .callsArgWith(3, null, "hello \\usepackage[random-options]{pstool}") + @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback + + it "should look at the file on disk", -> + @SafeReader.readFile + .calledWith("#{@compileDir}/#{@mainFile}") + .should.equal true + + it "should call the callback with true ", -> + @callback.calledWithExactly(null, true) + .should.equal true + describe "injectOutputFile", -> beforeEach -> @rootDir = "/mock" From 3aad472a837a2bdfc991390f846b9a7928830fc8 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 12 Oct 2018 10:49:54 +0100 Subject: [PATCH 405/709] improve log message --- app/coffee/TikzManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index 60cb1e34..5b80e6c7 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -32,6 +32,6 @@ module.exports = TikzManager = return callback(error) if error? fs.readFile path, "utf8", (error, content) -> return callback(error) if error? - logger.log compileDir: compileDir, mainFile: mainFile, "copied file to output.tex for tikz" + logger.log compileDir: compileDir, mainFile: mainFile, "copied file to output.tex as project uses packages which require it" # use wx flag to ensure that output file does not already exist fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback From 1481b4fe50f3968d82bdbe308fe2af1c1244b77d Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 15 Oct 2018 10:01:52 +0100 Subject: [PATCH 406/709] fix exception when content undefined in TikzManager --- app/coffee/TikzManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee index 5b80e6c7..22def278 100644 --- a/app/coffee/TikzManager.coffee +++ b/app/coffee/TikzManager.coffee @@ -22,7 +22,7 @@ module.exports = TikzManager = SafeReader.readFile path, 65536, "utf8", (error, content) -> return callback(error) if error? usesTikzExternalize = content?.indexOf("\\tikzexternalize") >= 0 - usesPsTool = content.indexOf("{pstool}") >= 0 + usesPsTool = content?.indexOf("{pstool}") >= 0 logger.log compileDir: compileDir, mainFile: mainFile, usesTikzExternalize:usesTikzExternalize, usesPsTool: usesPsTool, "checked for packages needing main file as output.tex" needsMainFile = (usesTikzExternalize || usesPsTool) callback null, needsMainFile From cd0a71caba000f37e68e266c54ab3aa0dbe6e344 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 22 Oct 2018 16:01:17 +0100 Subject: [PATCH 407/709] Add some notes on the CLSIs --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 270c06ac..9048afa7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,30 @@ A web api for compiling LaTeX documents in the cloud [![Build Status](https://travis-ci.org/sharelatex/clsi-sharelatex.png?branch=master)](https://travis-ci.org/sharelatex/clsi-sharelatex) +The CLSI provide a RESTful interface to traditional LaTeX tools (or, more generally, any command line tool for composing marked-up documents into a display format such as PDF or HTML). The CLSI listens on the following ports by default: + +* TCP/3009 - the RESTful interface +* TCP/3048 - reports load information to the `CLSI-lb` +* TCP/3049 - HTTP interface to control the CLSI service + +These defaults can be modified in `config/settings.defaults.coffee`. + +The provided `Dockerfile` builds a docker image which has the docker command line tools installed. The configuration in `docker-compose-config.yml` mounts the docker socket, in order that the CLSI container can talk to the docker host it is running in. This allows it to spin up `sibling containers` running our TEXLIVE image on the same docker host to perform the actual compiles. + +The CLSI can be configured through the following environment variables: + + * `DOCKER_RUNNER` - Set to true to use sibling containers + * `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary + * `COMPILES_HOST_DIR` - Working directory for LaTeX compiles + * `SQLITE_PATH` - Path to SQLite database + * `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` + * `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` + * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` + * `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` + * `STATSD_HOST` - The address of the Statsd service (used by the metrics module) + * `LISTEN_ADDRESS` - The address for the RESTful service to listen on. Set to `0.0.0.0` to listen on all network interfaces + * `SMOKE_TEST` - Whether to run smoke tests + Installation ------------ From 114e4f70435bc2632e9afb46e99e3247b5e850c6 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <mans0954@users.noreply.github.com> Date: Mon, 22 Oct 2018 16:03:50 +0100 Subject: [PATCH 408/709] Fix indenting --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9048afa7..9d7b08a0 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,17 @@ The provided `Dockerfile` builds a docker image which has the docker command lin The CLSI can be configured through the following environment variables: - * `DOCKER_RUNNER` - Set to true to use sibling containers - * `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary - * `COMPILES_HOST_DIR` - Working directory for LaTeX compiles - * `SQLITE_PATH` - Path to SQLite database - * `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` - * `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` - * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` - * `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` - * `STATSD_HOST` - The address of the Statsd service (used by the metrics module) - * `LISTEN_ADDRESS` - The address for the RESTful service to listen on. Set to `0.0.0.0` to listen on all network interfaces - * `SMOKE_TEST` - Whether to run smoke tests + * `DOCKER_RUNNER` - Set to true to use sibling containers + * `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary + * `COMPILES_HOST_DIR` - Working directory for LaTeX compiles + * `SQLITE_PATH` - Path to SQLite database + * `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` + * `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` + * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` + * `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` + * `STATSD_HOST` - The address of the Statsd service (used by the metrics module) + * `LISTEN_ADDRESS` - The address for the RESTful service to listen on. Set to `0.0.0.0` to listen on all network interfaces + * `SMOKE_TEST` - Whether to run smoke tests Installation ------------ From 3aa160b0e7f074d5c288535436d1e3475e93f0e0 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 22 Oct 2018 17:52:38 +0100 Subject: [PATCH 409/709] Make REAME more generic --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9048afa7..7b81e4dc 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ A web api for compiling LaTeX documents in the cloud The CLSI provide a RESTful interface to traditional LaTeX tools (or, more generally, any command line tool for composing marked-up documents into a display format such as PDF or HTML). The CLSI listens on the following ports by default: * TCP/3009 - the RESTful interface -* TCP/3048 - reports load information to the `CLSI-lb` +* TCP/3048 - reports load information * TCP/3049 - HTTP interface to control the CLSI service These defaults can be modified in `config/settings.defaults.coffee`. -The provided `Dockerfile` builds a docker image which has the docker command line tools installed. The configuration in `docker-compose-config.yml` mounts the docker socket, in order that the CLSI container can talk to the docker host it is running in. This allows it to spin up `sibling containers` running our TEXLIVE image on the same docker host to perform the actual compiles. +The provided `Dockerfile` builds a docker image which has the docker command line tools installed. The configuration in `docker-compose-config.yml` mounts the docker socket, in order that the CLSI container can talk to the docker host it is running in. This allows it to spin up `sibling containers` running an image with a TeX distribution installed to perform the actual compiles. The CLSI can be configured through the following environment variables: @@ -21,9 +21,9 @@ The CLSI can be configured through the following environment variables: * `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary * `COMPILES_HOST_DIR` - Working directory for LaTeX compiles * `SQLITE_PATH` - Path to SQLite database - * `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` + * `TEXLIVE_IMAGE` - The docker image with a TeX distribution to use for sibling containers * `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` - * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` + * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image * `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` * `STATSD_HOST` - The address of the Statsd service (used by the metrics module) * `LISTEN_ADDRESS` - The address for the RESTful service to listen on. Set to `0.0.0.0` to listen on all network interfaces From d3039a52f3a66d7ce29e9e0df28a60f78a8d03b5 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Wed, 7 Nov 2018 08:29:34 +0000 Subject: [PATCH 410/709] First attempt to use my stackdriver branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2183ef9..b646f7be 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.8.1", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#csh-stackdriver", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From a18d49562c19fe05078bee868bdd960f31353289 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 22 Nov 2018 09:13:23 +0000 Subject: [PATCH 411/709] Expand CLSI to Common LaTeX Service Interface on first use --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e074204c..ba0a63e4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A web api for compiling LaTeX documents in the cloud [![Build Status](https://travis-ci.org/sharelatex/clsi-sharelatex.png?branch=master)](https://travis-ci.org/sharelatex/clsi-sharelatex) -The CLSI provide a RESTful interface to traditional LaTeX tools (or, more generally, any command line tool for composing marked-up documents into a display format such as PDF or HTML). The CLSI listens on the following ports by default: +The Common LaTeX Service Interface (CLSI) provides a RESTful interface to traditional LaTeX tools (or, more generally, any command line tool for composing marked-up documents into a display format such as PDF or HTML). The CLSI listens on the following ports by default: * TCP/3009 - the RESTful interface * TCP/3048 - reports load information From 49f3b7d54f06e1946881f073d78a82f8f53d70d4 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 23 Nov 2018 14:52:13 +0000 Subject: [PATCH 412/709] have failed compiles warn rather than be an error --- app/coffee/CompileController.coffee | 4 ++-- test/unit/coffee/CompileControllerTests.coffee | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 5d1ee2e7..0a51c947 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -33,7 +33,7 @@ module.exports = CompileController = else status = "error" code = 500 - logger.error err: error, project_id: request.project_id, "error running compile" + logger.warn err: error, project_id: request.project_id, "error running compile" else status = "failure" @@ -42,7 +42,7 @@ module.exports = CompileController = status = "success" if status == "failure" - logger.err project_id: request.project_id, outputFiles:outputFiles, "project failed to compile successfully, no output.pdf generated" + logger.warn project_id: request.project_id, outputFiles:outputFiles, "project failed to compile successfully, no output.pdf generated" # log an error if any core files are found for file in outputFiles diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index f0269ee3..529c7322 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -14,7 +14,7 @@ describe "CompileController", -> clsi: url: "http://clsi.example.com" "./ProjectPersistenceManager": @ProjectPersistenceManager = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err:sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err:sinon.stub(), warn: sinon.stub()} @Settings.externalUrl = "http://www.example.com" @req = {} @res = {} From bcdac34a0b4922e688a6fa8a2981b705d0075aec Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 29 Nov 2018 10:10:48 +0000 Subject: [PATCH 413/709] Use v1.9.0 of metrics to get Prometheus support --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b646f7be..1bacde3d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#csh-stackdriver", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#1.9.0", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 49d5ad711aa341aafa7e975ee08b7fa53123dad8 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 29 Nov 2018 10:24:25 +0000 Subject: [PATCH 414/709] Bump metrics to v2.0.3 - specify tag correctly this time --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1bacde3d..3de282ab 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#1.9.0", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.3", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 6159aff00177d5443cd9052a5998c7809c2a22ee Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 29 Nov 2018 14:30:00 +0000 Subject: [PATCH 415/709] Inject metrics --- app.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app.coffee b/app.coffee index 367174f3..0db79dd5 100644 --- a/app.coffee +++ b/app.coffee @@ -16,6 +16,7 @@ Metrics = require "metrics-sharelatex" Metrics.initialize("clsi") Metrics.open_sockets.monitor(logger) Metrics.memory.monitor(logger) +Metrics.injectMetricsRoute(app) ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager" OutputCacheManager = require "./app/js/OutputCacheManager" From f92e6266471b773a82a4576340b88fd0fb1356e6 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 29 Nov 2018 15:49:12 +0000 Subject: [PATCH 416/709] Inject routes after app defined --- app.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.coffee b/app.coffee index 0db79dd5..10bceee5 100644 --- a/app.coffee +++ b/app.coffee @@ -16,7 +16,6 @@ Metrics = require "metrics-sharelatex" Metrics.initialize("clsi") Metrics.open_sockets.monitor(logger) Metrics.memory.monitor(logger) -Metrics.injectMetricsRoute(app) ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager" OutputCacheManager = require "./app/js/OutputCacheManager" @@ -27,6 +26,7 @@ express = require "express" bodyParser = require "body-parser" app = express() +Metrics.injectMetricsRoute(app) app.use Metrics.http.monitor(logger) # Compile requests can take longer than the default two From 1c1610a0bcc491576dd6d3918fed25e53608d2fb Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 3 Dec 2018 15:10:39 +0000 Subject: [PATCH 417/709] Bump metrics to 2.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3de282ab..e565b38b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.3", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.4", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 0b4ae6ef8d707a0a931756d3f048faa6859c7bd4 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 11 Dec 2018 12:11:53 +0000 Subject: [PATCH 418/709] Use metrics which labels host in timing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e565b38b..f0589b8c 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.4", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#d96892cc1741dd6d59e83c924a1458e4929d39b7", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 71181243b37279433344f48108bb489468cbc7e4 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 13 Dec 2018 14:15:19 +0000 Subject: [PATCH 419/709] Bump metrics-sharelatex.git to v2.0.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f0589b8c..84a3153a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#d96892cc1741dd6d59e83c924a1458e4929d39b7", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.11", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 8401bbdc26f062a8764632c1478189f623adad8c Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 13 Dec 2018 14:21:32 +0000 Subject: [PATCH 420/709] Bump metrics-sharelatex to v2.0.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84a3153a..49f5873e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.11", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.12", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 855f26c5205039e96cfea8d224eff5ebb743e046 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 13 Dec 2018 14:24:44 +0000 Subject: [PATCH 421/709] Initialise metrics at begining of app --- app.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app.coffee b/app.coffee index 10bceee5..d9faf9e3 100644 --- a/app.coffee +++ b/app.coffee @@ -1,3 +1,8 @@ +Metrics = require "metrics-sharelatex" +Metrics.initialize("clsi") +Metrics.open_sockets.monitor(logger) +Metrics.memory.monitor(logger) + CompileController = require "./app/js/CompileController" Settings = require "settings-sharelatex" logger = require "logger-sharelatex" @@ -12,11 +17,6 @@ Errors = require './app/js/Errors' Path = require "path" fs = require "fs" -Metrics = require "metrics-sharelatex" -Metrics.initialize("clsi") -Metrics.open_sockets.monitor(logger) -Metrics.memory.monitor(logger) - ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager" OutputCacheManager = require "./app/js/OutputCacheManager" From 38874f9169abfb1bcc7297988abe22c012a4c107 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 13 Dec 2018 14:45:40 +0000 Subject: [PATCH 422/709] Bump buildscript to 1.1.10 --- .github/ISSUE_TEMPLATE.md | 38 +++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 45 ++++++++++++++++++++++++++++++++ Dockerfile | 2 +- Makefile | 2 +- buildscript.txt | 9 +++++++ docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- package.json | 2 +- 8 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 buildscript.txt diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..e0093aa9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,38 @@ +<!-- BUG REPORT TEMPLATE --> + +## Steps to Reproduce +<!-- Describe the steps leading up to when / where you found the bug. --> +<!-- Screenshots may be helpful here. --> + +1. +2. +3. + +## Expected Behaviour +<!-- What should have happened when you completed the steps above? --> + +## Observed Behaviour +<!-- What actually happened when you completed the steps above? --> +<!-- Screenshots may be helpful here. --> + +## Context +<!-- How has this issue affected you? What were you trying to accomplish? --> + +## Technical Info +<!-- Provide any technical details that may be applicable (or N/A if not applicable). --> + +* URL: +* Browser Name and version: +* Operating System and version (desktop or mobile): +* Signed in as: +* Project and/or file: + +## Analysis +<!--- Optionally, document investigation of / suggest a fix for the bug, e.g. 'comes from this line / commit' --> + +## Who Needs to Know? +<!-- If you want to bring this to the attention of particular people, @-mention them below. --> +<!-- If a user reported this bug and should be notified when it is fixed, provide the Front conversation link. --> + +- +- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..ed25ee83 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ +<!-- Please review https://github.com/overleaf/write_latex/blob/master/.github/CONTRIBUTING.md for guidance on what is expected in each section. --> + +### Description + + + +#### Screenshots + + + +#### Related Issues / PRs + + + +### Review + + + +#### Potential Impact + + + +#### Manual Testing Performed + +- [ ] +- [ ] + +#### Accessibility + + + +### Deployment + + + +#### Deployment Checklist + +- [ ] Update documentation not included in the PR (if any) +- [ ] + +#### Metrics and Monitoring + + + +#### Who Needs to Know? diff --git a/Dockerfile b/Dockerfile index 1ccc689d..53f4b944 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,4 @@ WORKDIR /app RUN chmod 0755 ./install_deps.sh && ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] -CMD ["node","app.js"] +CMD ["node", "--expose-gc", "app.js"] diff --git a/Makefile b/Makefile index b1ce2934..7558b6e5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.9 +# Version: 1.1.10 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/buildscript.txt b/buildscript.txt new file mode 100644 index 00000000..e4a8e46f --- /dev/null +++ b/buildscript.txt @@ -0,0 +1,9 @@ +--script-version=1.1.10 +clsi +--node-version=6.14.1 +--acceptance-creds=None +--language=coffeescript +--dependencies=mongo,redis +--docker-repos=gcr.io/overleaf-ops +--kube=false +--build-target=docker diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index f6c8a27f..57108f50 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.9 +# Version: 1.1.10 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index 371e6e76..61bb8420 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.9 +# Version: 1.1.10 version: "2" diff --git a/package.json b/package.json index 49f5873e..0bc083c5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/sharelatex/clsi-sharelatex.git" }, "scripts": { - "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", + "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", From 19078fe866034aa259af21ff54e73c56fa193c9a Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 13 Dec 2018 17:33:45 +0000 Subject: [PATCH 423/709] Revert "Initialise metrics at begining of app" This reverts commit 855f26c5205039e96cfea8d224eff5ebb743e046. --- app.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app.coffee b/app.coffee index d9faf9e3..10bceee5 100644 --- a/app.coffee +++ b/app.coffee @@ -1,8 +1,3 @@ -Metrics = require "metrics-sharelatex" -Metrics.initialize("clsi") -Metrics.open_sockets.monitor(logger) -Metrics.memory.monitor(logger) - CompileController = require "./app/js/CompileController" Settings = require "settings-sharelatex" logger = require "logger-sharelatex" @@ -17,6 +12,11 @@ Errors = require './app/js/Errors' Path = require "path" fs = require "fs" +Metrics = require "metrics-sharelatex" +Metrics.initialize("clsi") +Metrics.open_sockets.monitor(logger) +Metrics.memory.monitor(logger) + ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager" OutputCacheManager = require "./app/js/OutputCacheManager" From 9507f0f80faf1ed1c8c03fd96ee17c88e81cc5ad Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 13 Dec 2018 17:37:16 +0000 Subject: [PATCH 424/709] Revert "Bump buildscript to 1.1.10" This reverts commit 38874f9169abfb1bcc7297988abe22c012a4c107. --- .github/ISSUE_TEMPLATE.md | 38 --------------------------- .github/PULL_REQUEST_TEMPLATE.md | 45 -------------------------------- Dockerfile | 2 +- Makefile | 2 +- buildscript.txt | 9 ------- docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- package.json | 2 +- 8 files changed, 5 insertions(+), 97 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 buildscript.txt diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index e0093aa9..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,38 +0,0 @@ -<!-- BUG REPORT TEMPLATE --> - -## Steps to Reproduce -<!-- Describe the steps leading up to when / where you found the bug. --> -<!-- Screenshots may be helpful here. --> - -1. -2. -3. - -## Expected Behaviour -<!-- What should have happened when you completed the steps above? --> - -## Observed Behaviour -<!-- What actually happened when you completed the steps above? --> -<!-- Screenshots may be helpful here. --> - -## Context -<!-- How has this issue affected you? What were you trying to accomplish? --> - -## Technical Info -<!-- Provide any technical details that may be applicable (or N/A if not applicable). --> - -* URL: -* Browser Name and version: -* Operating System and version (desktop or mobile): -* Signed in as: -* Project and/or file: - -## Analysis -<!--- Optionally, document investigation of / suggest a fix for the bug, e.g. 'comes from this line / commit' --> - -## Who Needs to Know? -<!-- If you want to bring this to the attention of particular people, @-mention them below. --> -<!-- If a user reported this bug and should be notified when it is fixed, provide the Front conversation link. --> - -- -- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index ed25ee83..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,45 +0,0 @@ -<!-- Please review https://github.com/overleaf/write_latex/blob/master/.github/CONTRIBUTING.md for guidance on what is expected in each section. --> - -### Description - - - -#### Screenshots - - - -#### Related Issues / PRs - - - -### Review - - - -#### Potential Impact - - - -#### Manual Testing Performed - -- [ ] -- [ ] - -#### Accessibility - - - -### Deployment - - - -#### Deployment Checklist - -- [ ] Update documentation not included in the PR (if any) -- [ ] - -#### Metrics and Monitoring - - - -#### Who Needs to Know? diff --git a/Dockerfile b/Dockerfile index 53f4b944..1ccc689d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,4 @@ WORKDIR /app RUN chmod 0755 ./install_deps.sh && ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] -CMD ["node", "--expose-gc", "app.js"] +CMD ["node","app.js"] diff --git a/Makefile b/Makefile index 7558b6e5..b1ce2934 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.10 +# Version: 1.1.9 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/buildscript.txt b/buildscript.txt deleted file mode 100644 index e4a8e46f..00000000 --- a/buildscript.txt +++ /dev/null @@ -1,9 +0,0 @@ ---script-version=1.1.10 -clsi ---node-version=6.14.1 ---acceptance-creds=None ---language=coffeescript ---dependencies=mongo,redis ---docker-repos=gcr.io/overleaf-ops ---kube=false ---build-target=docker diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 57108f50..f6c8a27f 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.10 +# Version: 1.1.9 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index 61bb8420..371e6e76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.10 +# Version: 1.1.9 version: "2" diff --git a/package.json b/package.json index 0bc083c5..49f5873e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/sharelatex/clsi-sharelatex.git" }, "scripts": { - "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", + "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", From 2d023a3b0385e5df487a55bf6300a098c8194431 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 17 Dec 2018 15:29:56 +0000 Subject: [PATCH 425/709] Bump node to 6.15.1 --- .nvmrc | 2 +- Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nvmrc b/.nvmrc index bbf0c5a5..d36e8d82 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.14.1 +6.15.1 diff --git a/Dockerfile b/Dockerfile index 1ccc689d..ea550b28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:6.14.1 as app +FROM node:6.15.1 as app WORKDIR /app @@ -12,7 +12,7 @@ COPY . /app RUN npm run compile:all -FROM node:6.14.1 +FROM node:6.15.1 COPY --from=app /app /app From be855805c9c7ebcf957f800e0a59ac6a5055e488 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 17 Dec 2018 15:31:45 +0000 Subject: [PATCH 426/709] package-lock not supported until npm 5 --- package-lock.json | 3002 --------------------------------------------- 1 file changed, 3002 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d58c1d73..00000000 --- a/package-lock.json +++ /dev/null @@ -1,3002 +0,0 @@ -{ - "name": "node-clsi", - "version": "0.1.4", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@types/geojson": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", - "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" - }, - "@types/node": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.4.tgz", - "integrity": "sha512-8TqvB0ReZWwtcd3LXq3YSrBoLyXFgBX/sBZfGye9+YS8zH7/g+i6QRIuiDmwBoTzcQ/pk89nZYTYU4c5akKkzw==" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", - "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "argparse": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", - "dev": true, - "requires": { - "underscore": "1.7.0", - "underscore.string": "2.4.0" - }, - "dependencies": { - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", - "dev": true - }, - "underscore.string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", - "dev": true - } - } - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", - "dev": true - }, - "async": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", - "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "bignumber.js": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", - "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "2.3.6", - "safe-buffer": "5.1.2" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "2.0.3" - } - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "1.0.4", - "debug": "2.6.9", - "depd": "1.1.2", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.15" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.4.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - } - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.17" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - } - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "1.1.0", - "buffer-fill": "1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "bunyan": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", - "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", - "dev": true, - "requires": { - "mv": "2.1.1" - } - }, - "buster-core": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", - "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", - "dev": true - }, - "buster-format": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", - "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", - "dev": true, - "requires": { - "buster-core": "0.6.4" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", - "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", - "dev": true, - "requires": { - "assertion-error": "1.0.0", - "deep-eql": "0.1.3" - } - }, - "chalk": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", - "integrity": "sha1-HJhDdzfxGZ68wdTEj9Qbn5yOjyM=", - "dev": true, - "requires": { - "ansi-styles": "0.2.0", - "has-color": "0.1.7" - } - }, - "chownr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" - }, - "cls-bluebird": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", - "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", - "requires": { - "is-bluebird": "1.0.2", - "shimmer": "1.2.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "coffee-script": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "dev": true - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "commander": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.0.6", - "typedarray": "0.0.6" - }, - "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "dateformat": { - "version": "1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "requires": { - "ms": "2.0.0" - } - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "diff": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" - }, - "docker-modem": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.6.tgz", - "integrity": "sha512-kDwWa5QaiVMB8Orbb7nXdGdwEZHKfEm7iPwglXe1KorImMpmGNlhC7A5LG0p8rrCcz1J4kJhq/o63lFjDdj8rQ==", - "requires": { - "debug": "3.1.0", - "JSONStream": "1.3.2", - "readable-stream": "1.0.34", - "split-ca": "1.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "dockerode": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.5.tgz", - "integrity": "sha512-H3HX18xKmy51wqpPHvGDwPOotJMy9l/AWfiaVu4imrgBGr384rINEB2FwTwoYU++krkZjseVYyiVK8CnRz2tkw==", - "requires": { - "concat-stream": "1.5.2", - "docker-modem": "1.0.6", - "tar-fs": "1.12.0" - } - }, - "dottie": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.0.tgz", - "integrity": "sha1-2hkZgci41xPKARXViYzzl8Lw3dA=" - }, - "dtrace-provider": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", - "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", - "optional": true, - "requires": { - "nan": "2.10.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, - "requires": { - "jsbn": "0.1.1", - "safer-buffer": "2.1.2" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "1.4.0" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", - "dev": true - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "express": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", - "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", - "requires": { - "accepts": "1.3.4", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "1.1.2", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.0", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", - "qs": "6.5.1", - "range-parser": "1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.1", - "serve-static": "1.13.1", - "setprototypeof": "1.1.0", - "statuses": "1.3.1", - "type-is": "1.6.15", - "utils-merge": "1.0.1", - "vary": "1.1.2" - }, - "dependencies": { - "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", - "requires": { - "mime-types": "2.1.17", - "negotiator": "0.6.1" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "requires": { - "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "proxy-addr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.5.2" - } - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "send": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.2", - "destroy": "1.0.4", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" - } - }, - "serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", - "requires": { - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.1" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.17" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "findup-sync": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", - "dev": true, - "requires": { - "glob": "3.2.11", - "lodash": "2.4.2" - }, - "dependencies": { - "glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "minimatch": "0.3.0" - } - }, - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "minimatch": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - } - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.19" - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-extra": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", - "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", - "requires": { - "graceful-fs": "3.0.11", - "jsonfile": "2.4.0", - "rimraf": "2.6.2" - } - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha1-BsJ3IYRU7CiN93raVKA7hwKqy50=", - "requires": { - "minipass": "2.3.3" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - } - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.3" - } - }, - "generic-pool": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz", - "integrity": "sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag==" - }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "requires": { - "natives": "1.1.4" - } - }, - "growl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" - }, - "grunt": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", - "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", - "dev": true, - "requires": { - "async": "0.1.22", - "coffee-script": "1.3.3", - "colors": "0.6.2", - "dateformat": "1.0.2-1.2.3", - "eventemitter2": "0.4.14", - "exit": "0.1.2", - "findup-sync": "0.1.3", - "getobject": "0.1.0", - "glob": "3.1.21", - "grunt-legacy-log": "0.1.3", - "grunt-legacy-util": "0.2.0", - "hooker": "0.2.3", - "iconv-lite": "0.2.11", - "js-yaml": "2.0.5", - "lodash": "0.9.2", - "minimatch": "0.2.14", - "nopt": "1.0.10", - "rimraf": "2.2.8", - "underscore.string": "2.2.1", - "which": "1.0.9" - }, - "dependencies": { - "async": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", - "dev": true - }, - "coffee-script": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", - "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", - "dev": true - }, - "glob": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", - "dev": true, - "requires": { - "graceful-fs": "1.2.3", - "inherits": "1.0.2", - "minimatch": "0.2.14" - } - }, - "graceful-fs": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", - "dev": true - }, - "iconv-lite": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", - "dev": true - }, - "inherits": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", - "dev": true - }, - "lodash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", - "dev": true - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1.1.1" - } - }, - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true - } - } - }, - "grunt-bunyan": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", - "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", - "dev": true, - "requires": { - "lodash": "2.4.2" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - } - } - }, - "grunt-contrib-clean": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", - "integrity": "sha1-9T397ghJsce0Dp67umn0jExgecU=", - "dev": true, - "requires": { - "rimraf": "2.2.8" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true - } - } - }, - "grunt-contrib-coffee": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", - "integrity": "sha1-ixIme3TnM4sfKcW4txj7n4mYLxM=", - "dev": true, - "requires": { - "coffee-script": "1.6.3" - }, - "dependencies": { - "coffee-script": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", - "integrity": "sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4=", - "dev": true - } - } - }, - "grunt-execute": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", - "integrity": "sha1-yX64lDYS/vu3L749Mu+VIzxfouk=", - "dev": true - }, - "grunt-legacy-log": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", - "dev": true, - "requires": { - "colors": "0.6.2", - "grunt-legacy-log-utils": "0.1.1", - "hooker": "0.2.3", - "lodash": "2.4.2", - "underscore.string": "2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", - "dev": true - } - } - }, - "grunt-legacy-log-utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", - "dev": true, - "requires": { - "colors": "0.6.2", - "lodash": "2.4.2", - "underscore.string": "2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", - "dev": true - } - } - }, - "grunt-legacy-util": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", - "dev": true, - "requires": { - "async": "0.1.22", - "exit": "0.1.2", - "getobject": "0.1.0", - "hooker": "0.2.3", - "lodash": "0.9.2", - "underscore.string": "2.2.1", - "which": "1.0.9" - }, - "dependencies": { - "async": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", - "dev": true - }, - "lodash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", - "dev": true - } - } - }, - "grunt-mkdir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz", - "integrity": "sha1-c+GiasJKCFljY/TdlUsNMkheWOk=" - }, - "grunt-mocha-test": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", - "integrity": "sha1-emGEuYhg0Phb3qrWvqob199bvus=", - "dev": true, - "requires": { - "mocha": "1.14.0" - }, - "dependencies": { - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "dev": true, - "requires": { - "graceful-fs": "2.0.3", - "inherits": "2.0.3", - "minimatch": "0.2.14" - } - }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", - "dev": true - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "mocha": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", - "integrity": "sha1-cT223FAAGRqdA1gZXQkIeQ7LYVc=", - "dev": true, - "requires": { - "commander": "2.0.0", - "debug": "2.6.9", - "diff": "1.0.7", - "glob": "3.2.3", - "growl": "1.7.0", - "jade": "0.26.3", - "mkdirp": "0.3.5" - } - } - } - }, - "grunt-shell": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", - "integrity": "sha1-5KbRuSkSd2/ZOimcX2zGTpUlNlw=", - "dev": true, - "requires": { - "chalk": "0.3.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "has-color": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", - "dev": true - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "heapdump": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.9.tgz", - "integrity": "sha1-A8dOsN9dZ74Jgug0KbqcnSs7f3g=" - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha1-qD5i59JyrA47VRqqgoMaGbafgvg=", - "requires": { - "minimatch": "3.0.4" - } - }, - "inflection": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - }, - "dependencies": { - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" - }, - "is-bluebird": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", - "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" - } - } - }, - "js-yaml": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", - "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", - "dev": true, - "requires": { - "argparse": "0.1.16", - "esprima": "1.0.4" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "4.1.11" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "optional": true - } - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "JSONStream": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", - "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "lockfile": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", - "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", - "requires": { - "signal-exit": "3.0.2" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "logger-sharelatex": { - "version": "git+https://github.com/sharelatex/logger-sharelatex.git#9ee7b52eb2bbd8fcbb1e2c708587c1e93fd4c733", - "requires": { - "bunyan": "1.5.1", - "coffee-script": "1.4.0", - "raven": "1.2.1" - }, - "dependencies": { - "bunyan": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", - "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", - "requires": { - "dtrace-provider": "0.6.0", - "mv": "2.1.1", - "safe-json-stringify": "1.2.0" - } - }, - "coffee-script": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", - "integrity": "sha1-XjvIqsJsAajie/EHcixWVfWtfTY=" - } - } - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "lsmod": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" - }, - "lynx": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", - "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", - "requires": { - "mersenne": "0.0.4", - "statsd-parser": "0.0.4" - } - }, - "mersenne": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" - }, - "metrics-sharelatex": { - "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#e5356366b5b83997c8e1645b2e936af453381517", - "requires": { - "coffee-script": "1.6.0", - "lynx": "0.1.1", - "underscore": "1.6.0" - }, - "dependencies": { - "lynx": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", - "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", - "requires": { - "mersenne": "0.0.4", - "statsd-parser": "0.0.4" - } - }, - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" - } - } - }, - "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" - }, - "mime-types": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", - "requires": { - "mime-db": "1.35.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "minipass": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.3.tgz", - "integrity": "sha1-p9zIt7gz9dNodZzOVE3MtV9Q8jM=", - "requires": { - "safe-buffer": "5.1.2", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", - "integrity": "sha1-EeE2WM5GvDpwomeqxYNZ0eDCnOs=", - "requires": { - "minipass": "2.3.3" - } - }, - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" - }, - "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", - "dev": true, - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", - "mkdirp": "0.5.1", - "supports-color": "4.4.0" - }, - "dependencies": { - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", - "dev": true - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - } - } - }, - "moment": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", - "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" - }, - "moment-timezone": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz", - "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==", - "requires": { - "moment": "2.22.2" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "optional": true, - "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "optional": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "optional": true, - "requires": { - "glob": "6.0.4" - } - } - } - }, - "mysql": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", - "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", - "requires": { - "bignumber.js": "2.0.7", - "readable-stream": "1.1.14", - "require-all": "1.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" - }, - "natives": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.4.tgz", - "integrity": "sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg==" - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "optional": true - }, - "needle": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.1.tgz", - "integrity": "sha1-teMlvTqujCZ4kC+ilvcpRV0dOn0=", - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.23", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", - "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.1", - "nopt": "4.0.1", - "npm-packlist": "1.1.11", - "npmlog": "4.1.2", - "rc": "1.2.8", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.4" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - } - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", - "integrity": "sha1-fnFwPZc68zcKlZG6/jpjrKC+Iwg=" - }, - "npm-packlist": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", - "integrity": "sha1-hOjGg8vnhn00sdNX2JPOKeKKAt4=", - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", - "requires": { - "are-we-there-yet": "1.1.5", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "requires": { - "end-of-stream": "1.4.1", - "once": "1.4.0" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "raven": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", - "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", - "requires": { - "cookie": "0.3.1", - "json-stringify-safe": "5.0.1", - "lsmod": "1.0.0", - "stack-trace": "0.0.9", - "uuid": "3.0.0" - }, - "dependencies": { - "uuid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" - } - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "0.6.0", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.19", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" - } - }, - "require-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", - "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" - }, - "require-like": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", - "dev": true - }, - "retry-as-promised": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.3.2.tgz", - "integrity": "sha1-zZdO5P2bX+A8vzGHHuSCIcB3N7c=", - "requires": { - "bluebird": "3.5.1", - "debug": "2.6.9" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-json-stringify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", - "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sandboxed-module": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", - "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", - "dev": true, - "requires": { - "require-like": "0.1.2", - "stack-trace": "0.0.6" - }, - "dependencies": { - "stack-trace": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", - "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", - "dev": true - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=" - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=" - }, - "sequelize": { - "version": "4.38.0", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.38.0.tgz", - "integrity": "sha512-ZCcV2HuzU+03xunWgVeyXnPa/RYY5D2U/WUNpq+xF8VmDTLnSDsHl+pEwmiWrpZD7KdBqDczCeTgjToYyVzYQg==", - "requires": { - "bluebird": "3.5.1", - "cls-bluebird": "2.1.0", - "debug": "3.1.0", - "depd": "1.1.2", - "dottie": "2.0.0", - "generic-pool": "3.4.2", - "inflection": "1.12.0", - "lodash": "4.17.10", - "moment": "2.22.2", - "moment-timezone": "0.5.21", - "retry-as-promised": "2.3.2", - "semver": "5.5.0", - "terraformer-wkt-parser": "1.2.0", - "toposort-class": "1.0.1", - "uuid": "3.3.2", - "validator": "10.4.0", - "wkx": "0.4.5" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "settings-sharelatex": { - "version": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", - "requires": { - "coffee-script": "1.6.0" - } - }, - "shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-xTCx2vohXC2EWWDqY/zb4+5Mu28D+HYNSOuFzsyRDRvI/e1ICb69afwaUwfjr+25ZXldbOLyp+iDUZHq8UnTag==" - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "sinon": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", - "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", - "dev": true, - "requires": { - "buster-format": "0.5.6" - } - }, - "smoke-test-sharelatex": { - "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", - "requires": { - "mocha": "1.17.1" - }, - "dependencies": { - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "requires": { - "graceful-fs": "2.0.3", - "inherits": "2.0.3", - "minimatch": "0.2.14" - } - }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "mocha": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", - "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", - "requires": { - "commander": "2.0.0", - "debug": "2.6.9", - "diff": "1.0.7", - "glob": "3.2.3", - "growl": "1.7.0", - "jade": "0.26.3", - "mkdirp": "0.3.5" - } - } - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "requires": { - "hoek": "2.16.3" - } - }, - "split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" - }, - "sqlite3": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.2.tgz", - "integrity": "sha512-51ferIRwYOhzUEtogqOa/y9supADlAht98bF/gbIi6WkzRJX6Yioldxbzj1MV4yV+LgdKD/kkHwFTeFXOG4htA==", - "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.10.3", - "request": "2.87.0" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha1-MvACNc0I1IK00NaNuTqCnA7VdW4=", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.19", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" - } - } - } - }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.2", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" - } - }, - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - }, - "statsd-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", - "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - }, - "tar": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.4.tgz", - "integrity": "sha512-mq9ixIYfNF9SK0IS/h2HKMu8Q2iaCuhDDsZhdEag/FHv8fOaYld4vN7ouMgcSSt5WKZzPs8atclTcJm36OTh4w==", - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.3.3", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.2", - "yallist": "3.0.2" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - } - } - }, - "tar-fs": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", - "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", - "requires": { - "mkdirp": "0.5.1", - "pump": "1.0.3", - "tar-stream": "1.6.1" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - } - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.6", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - }, - "dependencies": { - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - } - } - }, - "tar-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", - "requires": { - "bl": "1.2.2", - "buffer-alloc": "1.2.0", - "end-of-stream": "1.4.1", - "fs-constants": "1.0.0", - "readable-stream": "2.3.6", - "to-buffer": "1.1.1", - "xtend": "4.0.1" - } - }, - "terraformer": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.9.tgz", - "integrity": "sha512-YlmQ1fsMWTkKGDGibCRWgmLzrpDRUr63Q025LJ/taYQ6j1Yb8q9McKF7NBi6ACAyUXO6F/bl9w6v4MY307y5Ag==", - "requires": { - "@types/geojson": "1.0.6" - } - }, - "terraformer-wkt-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz", - "integrity": "sha512-QU3iA54St5lF8Za1jg1oj4NYc8sn5tCZ08aNSWDeGzrsaV48eZk1iAVWasxhNspYBoCqdHuoot1pUTUrE1AJ4w==", - "requires": { - "@types/geojson": "1.0.6", - "terraformer": "1.0.9" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "timekeeper": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", - "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", - "dev": true - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "toposort-class": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", - "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha1-7GDO44rGdQY//JelwYlwV47oNlU=", - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" - }, - "underscore.string": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "v8-profiler": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", - "integrity": "sha1-6DgcvrtbX9DKjSsJ9qAYGhWNs00=", - "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.6.39" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.19" - } - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "node-pre-gyp": { - "version": "0.6.39", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "requires": { - "detect-libc": "1.0.3", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.8", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "2.2.1", - "tar-pack": "3.4.1" - } - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.19", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.2", - "stringstream": "0.0.6", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" - } - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - } - } - }, - "validator": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.4.0.tgz", - "integrity": "sha512-Q/wBy3LB1uOyssgNlXSRmaf22NxjvDNZM2MtIQ4jaEOAB61xsh1TQxsq1CgzUMBV1lDrVMogIh8GjG1DYW0zLg==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, - "which": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", - "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "1.0.2" - } - }, - "wkx": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.5.tgz", - "integrity": "sha512-01dloEcJZAJabLO5XdcRgqdKpmnxS0zIT02LhkdWOZX2Zs2tPM6hlZ4XG9tWaWur1Qd1OO4kJxUbe2+5BofvnA==", - "requires": { - "@types/node": "10.5.4" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "wrench": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", - "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" - } - } -} From 984474ee119fcabecfeffde20109ec9a277a21f3 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 18 Dec 2018 11:03:06 +0000 Subject: [PATCH 427/709] Add npm-shrinkwrap.json --- npm-shrinkwrap.json | 2499 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2499 insertions(+) create mode 100644 npm-shrinkwrap.json diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json new file mode 100644 index 00000000..4853442c --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,2499 @@ +{ + "name": "node-clsi", + "version": "0.1.4", + "dependencies": { + "@google-cloud/common": { + "version": "0.27.0", + "from": "@google-cloud/common@>=0.27.0 <0.28.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.27.0.tgz" + }, + "@google-cloud/debug-agent": { + "version": "3.0.1", + "from": "@google-cloud/debug-agent@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-3.0.1.tgz", + "dependencies": { + "coffeescript": { + "version": "2.3.2", + "from": "coffeescript@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.3.2.tgz" + } + } + }, + "@google-cloud/profiler": { + "version": "0.2.3", + "from": "@google-cloud/profiler@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-0.2.3.tgz", + "dependencies": { + "@google-cloud/common": { + "version": "0.26.2", + "from": "@google-cloud/common@>=0.26.0 <0.27.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.26.2.tgz" + }, + "through2": { + "version": "3.0.0", + "from": "through2@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz" + } + } + }, + "@google-cloud/projectify": { + "version": "0.3.2", + "from": "@google-cloud/projectify@>=0.3.2 <0.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.2.tgz" + }, + "@google-cloud/promisify": { + "version": "0.3.1", + "from": "@google-cloud/promisify@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz" + }, + "@google-cloud/trace-agent": { + "version": "3.5.0", + "from": "@google-cloud/trace-agent@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-3.5.0.tgz", + "dependencies": { + "@google-cloud/common": { + "version": "0.28.0", + "from": "@google-cloud/common@>=0.28.0 <0.29.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.28.0.tgz" + }, + "uuid": { + "version": "3.3.2", + "from": "uuid@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + } + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "from": "@protobufjs/aspromise@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "from": "@protobufjs/base64@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "from": "@protobufjs/codegen@>=2.0.4 <3.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "from": "@protobufjs/eventemitter@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "from": "@protobufjs/fetch@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" + }, + "@protobufjs/float": { + "version": "1.0.2", + "from": "@protobufjs/float@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "from": "@protobufjs/inquire@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" + }, + "@protobufjs/path": { + "version": "1.1.2", + "from": "@protobufjs/path@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "from": "@protobufjs/pool@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "from": "@protobufjs/utf8@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" + }, + "@sindresorhus/is": { + "version": "0.13.0", + "from": "@sindresorhus/is@>=0.13.0 <0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.13.0.tgz" + }, + "@types/caseless": { + "version": "0.12.1", + "from": "@types/caseless@*", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz" + }, + "@types/console-log-level": { + "version": "1.4.0", + "from": "@types/console-log-level@>=1.4.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz" + }, + "@types/duplexify": { + "version": "3.6.0", + "from": "@types/duplexify@>=3.5.0 <4.0.0", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz" + }, + "@types/form-data": { + "version": "2.2.1", + "from": "@types/form-data@*", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz" + }, + "@types/geojson": { + "version": "1.0.6", + "from": "@types/geojson@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz" + }, + "@types/long": { + "version": "4.0.0", + "from": "@types/long@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz" + }, + "@types/node": { + "version": "10.12.15", + "from": "@types/node@*", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz" + }, + "@types/request": { + "version": "2.48.1", + "from": "@types/request@>=2.47.0 <3.0.0", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz" + }, + "@types/semver": { + "version": "5.5.0", + "from": "@types/semver@>=5.5.0 <6.0.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz" + }, + "@types/tough-cookie": { + "version": "2.3.4", + "from": "@types/tough-cookie@*", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz" + }, + "abbrev": { + "version": "1.1.1", + "from": "abbrev@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + }, + "accepts": { + "version": "1.3.5", + "from": "accepts@>=1.3.5 <1.4.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz" + }, + "acorn": { + "version": "5.7.3", + "from": "acorn@>=5.0.3 <6.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz" + }, + "agent-base": { + "version": "4.2.1", + "from": "agent-base@>=4.1.0 <5.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz" + }, + "ajv": { + "version": "6.6.2", + "from": "ajv@>=6.5.5 <7.0.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz" + }, + "ansi-regex": { + "version": "2.1.1", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + }, + "ansi-styles": { + "version": "0.2.0", + "from": "ansi-styles@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "from": "aproba@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz" + }, + "are-we-there-yet": { + "version": "1.1.5", + "from": "are-we-there-yet@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz" + }, + "argparse": { + "version": "0.1.16", + "from": "argparse@>=0.1.11 <0.2.0", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "dev": true, + "dependencies": { + "underscore": { + "version": "1.7.0", + "from": "underscore@>=1.7.0 <1.8.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "dev": true + }, + "underscore.string": { + "version": "2.4.0", + "from": "underscore.string@>=2.4.0 <2.5.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "dev": true + } + } + }, + "array-flatten": { + "version": "1.1.1", + "from": "array-flatten@1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + }, + "arrify": { + "version": "1.0.1", + "from": "arrify@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + }, + "asn1": { + "version": "0.2.4", + "from": "asn1@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz" + }, + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + }, + "assertion-error": { + "version": "1.0.0", + "from": "assertion-error@1.0.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "dev": true + }, + "async": { + "version": "0.2.9", + "from": "async@0.2.9", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz" + }, + "async-listener": { + "version": "0.6.10", + "from": "async-listener@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz" + }, + "asynckit": { + "version": "0.4.0", + "from": "asynckit@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + }, + "aws-sign2": { + "version": "0.7.0", + "from": "aws-sign2@>=0.7.0 <0.8.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + }, + "aws4": { + "version": "1.8.0", + "from": "aws4@>=1.8.0 <2.0.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz" + }, + "axios": { + "version": "0.18.0", + "from": "axios@>=0.18.0 <0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz" + }, + "balanced-match": { + "version": "1.0.0", + "from": "balanced-match@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "from": "bcrypt-pbkdf@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + }, + "bignumber.js": { + "version": "7.2.1", + "from": "bignumber.js@>=7.0.0 <8.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz" + }, + "bindings": { + "version": "1.3.1", + "from": "bindings@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz" + }, + "bintrees": { + "version": "1.0.1", + "from": "bintrees@1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz" + }, + "bl": { + "version": "1.2.2", + "from": "bl@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz" + }, + "block-stream": { + "version": "0.0.9", + "from": "block-stream@*", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz" + }, + "bluebird": { + "version": "3.5.3", + "from": "bluebird@>=3.5.0 <4.0.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz" + }, + "body-parser": { + "version": "1.18.3", + "from": "body-parser@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz" + }, + "boom": { + "version": "2.10.1", + "from": "boom@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + }, + "brace-expansion": { + "version": "1.1.11", + "from": "brace-expansion@>=1.1.7 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + }, + "browser-stdout": { + "version": "1.3.0", + "from": "browser-stdout@1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "dev": true + }, + "buffer-alloc": { + "version": "1.2.0", + "from": "buffer-alloc@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz" + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "from": "buffer-alloc-unsafe@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "from": "buffer-equal-constant-time@1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" + }, + "buffer-fill": { + "version": "1.0.0", + "from": "buffer-fill@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz" + }, + "buffer-from": { + "version": "1.1.1", + "from": "buffer-from@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz" + }, + "builtin-modules": { + "version": "3.0.0", + "from": "builtin-modules@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.0.0.tgz" + }, + "bunyan": { + "version": "0.22.3", + "from": "bunyan@>=0.22.1 <0.23.0", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", + "dev": true + }, + "buster-core": { + "version": "0.6.4", + "from": "buster-core@0.6.4", + "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", + "dev": true + }, + "buster-format": { + "version": "0.5.6", + "from": "buster-format@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "from": "bytes@3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + }, + "caseless": { + "version": "0.12.0", + "from": "caseless@>=0.12.0 <0.13.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + }, + "chai": { + "version": "1.8.1", + "from": "chai@>=1.8.1 <1.9.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", + "dev": true + }, + "chalk": { + "version": "0.3.0", + "from": "chalk@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", + "dev": true + }, + "chownr": { + "version": "1.1.1", + "from": "chownr@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz" + }, + "cls-bluebird": { + "version": "2.1.0", + "from": "cls-bluebird@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz" + }, + "co": { + "version": "4.6.0", + "from": "co@>=4.6.0 <5.0.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + }, + "code-point-at": { + "version": "1.1.0", + "from": "code-point-at@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" + }, + "coffee-script": { + "version": "1.6.0", + "from": "coffee-script@1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "dev": true + }, + "colors": { + "version": "0.6.2", + "from": "colors@>=0.6.2 <0.7.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "dev": true + }, + "combined-stream": { + "version": "1.0.7", + "from": "combined-stream@>=1.0.6 <1.1.0", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz" + }, + "commander": { + "version": "2.0.0", + "from": "commander@2.0.0", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + }, + "concat-stream": { + "version": "1.6.2", + "from": "concat-stream@>=1.6.2 <1.7.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz" + }, + "console-control-strings": { + "version": "1.1.0", + "from": "console-control-strings@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + }, + "console-log-level": { + "version": "1.4.0", + "from": "console-log-level@>=1.4.0 <2.0.0", + "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.0.tgz" + }, + "content-disposition": { + "version": "0.5.2", + "from": "content-disposition@0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz" + }, + "content-type": { + "version": "1.0.4", + "from": "content-type@>=1.0.4 <1.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + }, + "continuation-local-storage": { + "version": "3.2.1", + "from": "continuation-local-storage@>=3.2.1 <4.0.0", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz" + }, + "cookie": { + "version": "0.3.1", + "from": "cookie@0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz" + }, + "cookie-signature": { + "version": "1.0.6", + "from": "cookie-signature@1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + }, + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + }, + "cryptiles": { + "version": "2.0.5", + "from": "cryptiles@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + }, + "dashdash": { + "version": "1.14.1", + "from": "dashdash@>=1.12.0 <2.0.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "from": "dateformat@1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "dev": true + }, + "debug": { + "version": "2.6.9", + "from": "debug@2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + }, + "deep-eql": { + "version": "0.1.3", + "from": "deep-eql@0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "dev": true + }, + "deep-extend": { + "version": "0.6.0", + "from": "deep-extend@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + }, + "delay": { + "version": "4.1.0", + "from": "delay@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-4.1.0.tgz" + }, + "delayed-stream": { + "version": "1.0.0", + "from": "delayed-stream@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + }, + "delegates": { + "version": "1.0.0", + "from": "delegates@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + }, + "depd": { + "version": "1.1.2", + "from": "depd@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + }, + "destroy": { + "version": "1.0.4", + "from": "destroy@>=1.0.4 <1.1.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" + }, + "detect-libc": { + "version": "1.0.3", + "from": "detect-libc@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz" + }, + "diff": { + "version": "1.0.7", + "from": "diff@1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "dev": true + }, + "docker-modem": { + "version": "1.0.7", + "from": "docker-modem@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.7.tgz", + "dependencies": { + "debug": { + "version": "3.2.6", + "from": "debug@>=3.2.5 <4.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "ms": { + "version": "2.1.1", + "from": "ms@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.26-4 <1.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + } + } + }, + "dockerode": { + "version": "2.5.7", + "from": "dockerode@>=2.5.3 <3.0.0", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.7.tgz" + }, + "dottie": { + "version": "2.0.1", + "from": "dottie@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz" + }, + "dtrace-provider": { + "version": "0.2.8", + "from": "dtrace-provider@0.2.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", + "dev": true, + "optional": true + }, + "duplexify": { + "version": "3.6.1", + "from": "duplexify@>=3.6.0 <4.0.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz" + }, + "ecc-jsbn": { + "version": "0.1.2", + "from": "ecc-jsbn@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + }, + "ecdsa-sig-formatter": { + "version": "1.0.10", + "from": "ecdsa-sig-formatter@1.0.10", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz" + }, + "ee-first": { + "version": "1.1.1", + "from": "ee-first@1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + }, + "emitter-listener": { + "version": "1.1.2", + "from": "emitter-listener@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz" + }, + "encodeurl": { + "version": "1.0.2", + "from": "encodeurl@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + }, + "end-of-stream": { + "version": "1.4.1", + "from": "end-of-stream@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz" + }, + "ent": { + "version": "2.2.0", + "from": "ent@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz" + }, + "es6-promise": { + "version": "4.2.5", + "from": "es6-promise@>=4.0.3 <5.0.0", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz" + }, + "es6-promisify": { + "version": "5.0.0", + "from": "es6-promisify@>=5.0.0 <6.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz" + }, + "escape-html": { + "version": "1.0.3", + "from": "escape-html@>=1.0.3 <1.1.0", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + }, + "escape-string-regexp": { + "version": "1.0.5", + "from": "escape-string-regexp@1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "dev": true + }, + "esprima": { + "version": "1.0.4", + "from": "esprima@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "dev": true + }, + "etag": { + "version": "1.8.1", + "from": "etag@>=1.8.1 <1.9.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + }, + "eventemitter2": { + "version": "0.4.14", + "from": "eventemitter2@>=0.4.13 <0.5.0", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "dev": true + }, + "exit": { + "version": "0.1.2", + "from": "exit@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "dev": true + }, + "express": { + "version": "4.16.4", + "from": "express@>=4.2.0 <5.0.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "dependencies": { + "statuses": { + "version": "1.4.0", + "from": "statuses@>=1.4.0 <1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + } + } + }, + "extend": { + "version": "3.0.2", + "from": "extend@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + }, + "extsprintf": { + "version": "1.3.0", + "from": "extsprintf@1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + }, + "fast-deep-equal": { + "version": "2.0.1", + "from": "fast-deep-equal@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "from": "fast-json-stable-stringify@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz" + }, + "finalhandler": { + "version": "1.1.1", + "from": "finalhandler@1.1.1", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "dependencies": { + "statuses": { + "version": "1.4.0", + "from": "statuses@>=1.4.0 <1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + } + } + }, + "findit2": { + "version": "2.2.3", + "from": "findit2@>=2.2.3 <3.0.0", + "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz" + }, + "findup-sync": { + "version": "0.1.3", + "from": "findup-sync@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "dev": true, + "dependencies": { + "glob": { + "version": "3.2.11", + "from": "glob@>=3.2.9 <3.3.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "dev": true + }, + "lodash": { + "version": "2.4.2", + "from": "lodash@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "from": "minimatch@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "dev": true + } + } + }, + "follow-redirects": { + "version": "1.5.10", + "from": "follow-redirects@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "dependencies": { + "debug": { + "version": "3.1.0", + "from": "debug@3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" + } + } + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "form-data": { + "version": "2.3.3", + "from": "form-data@>=2.3.2 <2.4.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + }, + "forwarded": { + "version": "0.1.2", + "from": "forwarded@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz" + }, + "fresh": { + "version": "0.5.2", + "from": "fresh@0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + }, + "fs-constants": { + "version": "1.0.0", + "from": "fs-constants@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + }, + "fs-extra": { + "version": "0.16.5", + "from": "fs-extra@>=0.16.3 <0.17.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz" + }, + "fs-minipass": { + "version": "1.2.5", + "from": "fs-minipass@>=1.2.5 <2.0.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz" + }, + "fs.realpath": { + "version": "1.0.0", + "from": "fs.realpath@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + }, + "fstream": { + "version": "1.0.11", + "from": "fstream@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "dependencies": { + "graceful-fs": { + "version": "4.1.15", + "from": "graceful-fs@>=4.1.2 <5.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 >=0.0.0 <1.0.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + } + } + }, + "fstream-ignore": { + "version": "1.0.5", + "from": "fstream-ignore@>=1.0.5 <2.0.0", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz" + }, + "gauge": { + "version": "2.7.4", + "from": "gauge@>=2.7.3 <2.8.0", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz" + }, + "gaxios": { + "version": "1.0.4", + "from": "gaxios@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.0.4.tgz" + }, + "gcp-metadata": { + "version": "0.9.3", + "from": "gcp-metadata@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz" + }, + "generic-pool": { + "version": "3.4.2", + "from": "generic-pool@>=3.4.0 <4.0.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz" + }, + "getobject": { + "version": "0.1.0", + "from": "getobject@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "from": "getpass@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + }, + "glob": { + "version": "7.1.3", + "from": "glob@>=7.0.5 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz" + }, + "google-auth-library": { + "version": "2.0.2", + "from": "google-auth-library@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-2.0.2.tgz", + "dependencies": { + "gcp-metadata": { + "version": "0.7.0", + "from": "gcp-metadata@>=0.7.0 <0.8.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz" + } + } + }, + "google-p12-pem": { + "version": "1.0.3", + "from": "google-p12-pem@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.3.tgz" + }, + "graceful-fs": { + "version": "3.0.11", + "from": "graceful-fs@>=3.0.5 <4.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz" + }, + "growl": { + "version": "1.7.0", + "from": "growl@1.7.x", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "dev": true + }, + "grunt": { + "version": "0.4.5", + "from": "grunt@>=0.4.2 <0.5.0", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "dev": true, + "dependencies": { + "async": { + "version": "0.1.22", + "from": "async@>=0.1.22 <0.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "dev": true + }, + "coffee-script": { + "version": "1.3.3", + "from": "coffee-script@>=1.3.3 <1.4.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "dev": true + }, + "glob": { + "version": "3.1.21", + "from": "glob@>=3.1.21 <3.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "dev": true + }, + "graceful-fs": { + "version": "1.2.3", + "from": "graceful-fs@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "from": "iconv-lite@>=0.2.11 <0.3.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "from": "inherits@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "dev": true + }, + "lodash": { + "version": "0.9.2", + "from": "lodash@>=0.9.2 <0.10.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@>=0.2.12 <0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "dev": true + }, + "nopt": { + "version": "1.0.10", + "from": "nopt@>=1.0.10 <1.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "dev": true + }, + "rimraf": { + "version": "2.2.8", + "from": "rimraf@>=2.2.8 <2.3.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "dev": true + } + } + }, + "grunt-bunyan": { + "version": "0.5.0", + "from": "grunt-bunyan@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", + "dev": true, + "dependencies": { + "lodash": { + "version": "2.4.2", + "from": "lodash@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "dev": true + } + } + }, + "grunt-contrib-clean": { + "version": "0.5.0", + "from": "grunt-contrib-clean@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", + "dev": true, + "dependencies": { + "rimraf": { + "version": "2.2.8", + "from": "rimraf@>=2.2.1 <2.3.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "dev": true + } + } + }, + "grunt-contrib-coffee": { + "version": "0.7.0", + "from": "grunt-contrib-coffee@>=0.7.0 <0.8.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", + "dev": true, + "dependencies": { + "coffee-script": { + "version": "1.6.3", + "from": "coffee-script@>=1.6.2 <1.7.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", + "dev": true + } + } + }, + "grunt-execute": { + "version": "0.1.5", + "from": "grunt-execute@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", + "dev": true + }, + "grunt-legacy-log": { + "version": "0.1.3", + "from": "grunt-legacy-log@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "dev": true, + "dependencies": { + "lodash": { + "version": "2.4.2", + "from": "lodash@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "from": "underscore.string@>=2.3.3 <2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "from": "grunt-legacy-log-utils@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "dev": true, + "dependencies": { + "lodash": { + "version": "2.4.2", + "from": "lodash@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "from": "underscore.string@>=2.3.3 <2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "from": "grunt-legacy-util@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "dev": true, + "dependencies": { + "async": { + "version": "0.1.22", + "from": "async@>=0.1.22 <0.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "dev": true + }, + "lodash": { + "version": "0.9.2", + "from": "lodash@>=0.9.2 <0.10.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "dev": true + } + } + }, + "grunt-mkdir": { + "version": "1.0.0", + "from": "grunt-mkdir@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz" + }, + "grunt-mocha-test": { + "version": "0.8.2", + "from": "grunt-mocha-test@>=0.8.1 <0.9.0", + "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", + "dev": true, + "dependencies": { + "glob": { + "version": "3.2.3", + "from": "glob@3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "dev": true + }, + "graceful-fs": { + "version": "2.0.3", + "from": "graceful-fs@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@>=0.2.11 <0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "dev": true + }, + "mocha": { + "version": "1.14.0", + "from": "mocha@>=1.14.0 <1.15.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", + "dev": true + } + } + }, + "grunt-shell": { + "version": "0.6.4", + "from": "grunt-shell@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", + "dev": true + }, + "gtoken": { + "version": "2.3.0", + "from": "gtoken@>=2.3.0 <3.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.0.tgz", + "dependencies": { + "mime": { + "version": "2.4.0", + "from": "mime@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz" + }, + "pify": { + "version": "3.0.0", + "from": "pify@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" + } + } + }, + "har-schema": { + "version": "2.0.0", + "from": "har-schema@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + }, + "har-validator": { + "version": "5.1.3", + "from": "har-validator@>=5.1.0 <5.2.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz" + }, + "has-color": { + "version": "0.1.7", + "from": "has-color@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "from": "has-flag@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "from": "has-unicode@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" + }, + "hawk": { + "version": "3.1.3", + "from": "hawk@3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" + }, + "he": { + "version": "1.1.1", + "from": "he@1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "dev": true + }, + "heapdump": { + "version": "0.3.12", + "from": "heapdump@>=0.3.5 <0.4.0", + "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.12.tgz" + }, + "hex2dec": { + "version": "1.1.1", + "from": "hex2dec@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.1.tgz" + }, + "hoek": { + "version": "2.16.3", + "from": "hoek@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + }, + "hooker": { + "version": "0.2.3", + "from": "hooker@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "from": "http-errors@>=1.6.3 <1.7.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + }, + "http-signature": { + "version": "1.2.0", + "from": "http-signature@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + }, + "https-proxy-agent": { + "version": "2.2.1", + "from": "https-proxy-agent@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "dependencies": { + "debug": { + "version": "3.2.6", + "from": "debug@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" + }, + "ms": { + "version": "2.1.1", + "from": "ms@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + } + } + }, + "iconv-lite": { + "version": "0.4.23", + "from": "iconv-lite@0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz" + }, + "ignore-walk": { + "version": "3.0.1", + "from": "ignore-walk@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz" + }, + "inflection": { + "version": "1.12.0", + "from": "inflection@1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz" + }, + "inflight": { + "version": "1.0.6", + "from": "inflight@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + }, + "inherits": { + "version": "2.0.3", + "from": "inherits@2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + }, + "ini": { + "version": "1.3.5", + "from": "ini@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz" + }, + "ipaddr.js": { + "version": "1.8.0", + "from": "ipaddr.js@1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz" + }, + "is": { + "version": "3.3.0", + "from": "is@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz" + }, + "is-bluebird": { + "version": "1.0.2", + "from": "is-bluebird@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz" + }, + "is-buffer": { + "version": "1.1.6", + "from": "is-buffer@>=1.1.5 <2.0.0", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" + }, + "is-typedarray": { + "version": "1.0.0", + "from": "is-typedarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "jade": { + "version": "0.26.3", + "from": "jade@0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "dev": true, + "dependencies": { + "commander": { + "version": "0.6.1", + "from": "commander@0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "from": "mkdirp@0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "dev": true + } + } + }, + "js-yaml": { + "version": "2.0.5", + "from": "js-yaml@>=2.0.5 <2.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "dev": true + }, + "jsbn": { + "version": "0.1.1", + "from": "jsbn@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + }, + "json-bigint": { + "version": "0.3.0", + "from": "json-bigint@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz" + }, + "json-schema": { + "version": "0.2.3", + "from": "json-schema@0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz" + }, + "json-schema-traverse": { + "version": "0.4.1", + "from": "json-schema-traverse@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + }, + "json-stable-stringify": { + "version": "1.0.1", + "from": "json-stable-stringify@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "jsonfile": { + "version": "2.4.0", + "from": "jsonfile@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "dependencies": { + "graceful-fs": { + "version": "4.1.15", + "from": "graceful-fs@>=4.1.6 <5.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "optional": true + } + } + }, + "jsonify": { + "version": "0.0.0", + "from": "jsonify@>=0.0.0 <0.1.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" + }, + "jsonparse": { + "version": "1.3.1", + "from": "jsonparse@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" + }, + "JSONStream": { + "version": "1.3.2", + "from": "JSONStream@1.3.2", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz" + }, + "jsprim": { + "version": "1.4.1", + "from": "jsprim@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz" + }, + "jwa": { + "version": "1.1.6", + "from": "jwa@>=1.1.5 <2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz" + }, + "jws": { + "version": "3.1.5", + "from": "jws@>=3.1.5 <4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz" + }, + "lockfile": { + "version": "1.0.4", + "from": "lockfile@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz" + }, + "lodash": { + "version": "4.17.11", + "from": "lodash@>=4.17.1 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz" + }, + "lodash.pickby": { + "version": "4.6.0", + "from": "lodash.pickby@>=4.6.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz" + }, + "logger-sharelatex": { + "version": "1.5.4", + "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", + "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#9ee7b52eb2bbd8fcbb1e2c708587c1e93fd4c733" + }, + "long": { + "version": "4.0.0", + "from": "long@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz" + }, + "lru-cache": { + "version": "5.1.1", + "from": "lru-cache@>=5.0.0 <6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + }, + "lsmod": { + "version": "1.0.0", + "from": "lsmod@1.0.0", + "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz" + }, + "lynx": { + "version": "0.0.11", + "from": "lynx@0.0.11", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz" + }, + "media-typer": { + "version": "0.3.0", + "from": "media-typer@0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + }, + "merge-descriptors": { + "version": "1.0.1", + "from": "merge-descriptors@1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + }, + "mersenne": { + "version": "0.0.4", + "from": "mersenne@>=0.0.3 <0.1.0", + "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz" + }, + "methods": { + "version": "1.1.2", + "from": "methods@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + }, + "metrics-sharelatex": { + "version": "2.0.12", + "from": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.12", + "resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#3ac1621ef049e2f2d88a83b3a41011333d609662", + "dependencies": { + "coffee-script": { + "version": "1.6.0", + "from": "coffee-script@1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" + }, + "lynx": { + "version": "0.1.1", + "from": "lynx@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz" + }, + "underscore": { + "version": "1.6.0", + "from": "underscore@>=1.6.0 <1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" + } + } + }, + "mime": { + "version": "1.4.1", + "from": "mime@1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz" + }, + "mime-db": { + "version": "1.37.0", + "from": "mime-db@>=1.37.0 <1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz" + }, + "mime-types": { + "version": "2.1.21", + "from": "mime-types@>=2.1.18 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz" + }, + "minimatch": { + "version": "3.0.4", + "from": "minimatch@>=3.0.4 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + }, + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "minipass": { + "version": "2.3.5", + "from": "minipass@>=2.3.4 <3.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz" + }, + "minizlib": { + "version": "1.2.1", + "from": "minizlib@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz" + }, + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + }, + "mocha": { + "version": "4.1.0", + "from": "mocha@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "dev": true, + "dependencies": { + "commander": { + "version": "2.11.0", + "from": "commander@2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "dev": true + }, + "debug": { + "version": "3.1.0", + "from": "debug@3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "dev": true + }, + "diff": { + "version": "3.3.1", + "from": "diff@3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "dev": true + }, + "glob": { + "version": "7.1.2", + "from": "glob@7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "dev": true + }, + "growl": { + "version": "1.10.3", + "from": "growl@1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dev": true + } + } + }, + "module-details-from-path": { + "version": "1.0.3", + "from": "module-details-from-path@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz" + }, + "moment": { + "version": "2.23.0", + "from": "moment@>=2.20.0 <3.0.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz" + }, + "moment-timezone": { + "version": "0.5.23", + "from": "moment-timezone@>=0.5.14 <0.6.0", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz" + }, + "ms": { + "version": "2.0.0", + "from": "ms@2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + }, + "mv": { + "version": "2.1.1", + "from": "mv@~2", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "dev": true, + "optional": true, + "dependencies": { + "glob": { + "version": "6.0.4", + "from": "glob@>=6.0.1 <7.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "dev": true, + "optional": true + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dev": true, + "optional": true + }, + "rimraf": { + "version": "2.4.5", + "from": "rimraf@>=2.4.0 <2.5.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "dev": true, + "optional": true + } + } + }, + "mysql": { + "version": "2.6.2", + "from": "mysql@2.6.2", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", + "dependencies": { + "bignumber.js": { + "version": "2.0.7", + "from": "bignumber.js@2.0.7", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@>=1.1.13 <1.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + } + } + }, + "nan": { + "version": "2.12.0", + "from": "nan@>=2.11.1 <3.0.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz" + }, + "natives": { + "version": "1.1.6", + "from": "natives@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz" + }, + "ncp": { + "version": "2.0.0", + "from": "ncp@~2.0.0", + "resolved": "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "from": "needle@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz" + }, + "negotiator": { + "version": "0.6.1", + "from": "negotiator@0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" + }, + "node-fetch": { + "version": "2.3.0", + "from": "node-fetch@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz" + }, + "node-forge": { + "version": "0.7.6", + "from": "node-forge@>=0.7.5 <0.8.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz" + }, + "node-pre-gyp": { + "version": "0.10.3", + "from": "node-pre-gyp@>=0.10.3 <0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + } + } + }, + "nopt": { + "version": "4.0.1", + "from": "nopt@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz" + }, + "npm-bundled": { + "version": "1.0.5", + "from": "npm-bundled@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz" + }, + "npm-packlist": { + "version": "1.1.12", + "from": "npm-packlist@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz" + }, + "npmlog": { + "version": "4.1.2", + "from": "npmlog@>=4.0.2 <5.0.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz" + }, + "number-is-nan": { + "version": "1.0.1", + "from": "number-is-nan@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + }, + "oauth-sign": { + "version": "0.9.0", + "from": "oauth-sign@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + }, + "object-assign": { + "version": "4.1.1", + "from": "object-assign@>=4.1.0 <5.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + }, + "on-finished": { + "version": "2.3.0", + "from": "on-finished@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + }, + "once": { + "version": "1.4.0", + "from": "once@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + }, + "os-homedir": { + "version": "1.0.2", + "from": "os-homedir@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz" + }, + "os-tmpdir": { + "version": "1.0.2", + "from": "os-tmpdir@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + }, + "osenv": { + "version": "0.1.5", + "from": "osenv@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz" + }, + "p-limit": { + "version": "2.0.0", + "from": "p-limit@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz" + }, + "p-try": { + "version": "2.0.0", + "from": "p-try@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz" + }, + "parse-duration": { + "version": "0.1.1", + "from": "parse-duration@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz" + }, + "parse-ms": { + "version": "2.0.0", + "from": "parse-ms@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.0.0.tgz" + }, + "parseurl": { + "version": "1.3.2", + "from": "parseurl@>=1.3.2 <1.4.0", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz" + }, + "path-is-absolute": { + "version": "1.0.1", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + }, + "path-parse": { + "version": "1.0.6", + "from": "path-parse@>=1.0.6 <2.0.0", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz" + }, + "path-to-regexp": { + "version": "0.1.7", + "from": "path-to-regexp@0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + }, + "performance-now": { + "version": "2.1.0", + "from": "performance-now@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + }, + "pify": { + "version": "4.0.1", + "from": "pify@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" + }, + "pretty-ms": { + "version": "4.0.0", + "from": "pretty-ms@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz" + }, + "process-nextick-args": { + "version": "2.0.0", + "from": "process-nextick-args@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz" + }, + "prom-client": { + "version": "11.2.0", + "from": "prom-client@>=11.1.3 <12.0.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.2.0.tgz" + }, + "protobufjs": { + "version": "6.8.8", + "from": "protobufjs@>=6.8.6 <6.9.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz" + }, + "proxy-addr": { + "version": "2.0.4", + "from": "proxy-addr@>=2.0.4 <2.1.0", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz" + }, + "psl": { + "version": "1.1.31", + "from": "psl@>=1.1.24 <2.0.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz" + }, + "pump": { + "version": "1.0.3", + "from": "pump@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz" + }, + "punycode": { + "version": "2.1.1", + "from": "punycode@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + }, + "qs": { + "version": "6.5.2", + "from": "qs@6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" + }, + "range-parser": { + "version": "1.2.0", + "from": "range-parser@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" + }, + "raven": { + "version": "1.2.1", + "from": "raven@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz" + }, + "raw-body": { + "version": "2.3.3", + "from": "raw-body@2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz" + }, + "rc": { + "version": "1.2.8", + "from": "rc@>=1.2.7 <2.0.0", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "dependencies": { + "minimist": { + "version": "1.2.0", + "from": "minimist@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" + } + } + }, + "readable-stream": { + "version": "2.3.6", + "from": "readable-stream@>=2.2.2 <3.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz" + }, + "request": { + "version": "2.88.0", + "from": "request@>=2.21.0 <3.0.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "dependencies": { + "uuid": { + "version": "3.3.2", + "from": "uuid@>=3.3.2 <4.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + } + } + }, + "require-all": { + "version": "1.0.0", + "from": "require-all@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz" + }, + "require-in-the-middle": { + "version": "3.1.0", + "from": "require-in-the-middle@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-3.1.0.tgz" + }, + "require-like": { + "version": "0.1.2", + "from": "require-like@0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "dev": true + }, + "resolve": { + "version": "1.9.0", + "from": "resolve@>=1.5.0 <2.0.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz" + }, + "retry-as-promised": { + "version": "2.3.2", + "from": "retry-as-promised@>=2.3.2 <3.0.0", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.3.2.tgz" + }, + "retry-axios": { + "version": "0.3.2", + "from": "retry-axios@0.3.2", + "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz" + }, + "retry-request": { + "version": "4.0.0", + "from": "retry-request@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.0.0.tgz" + }, + "rimraf": { + "version": "2.6.2", + "from": "rimraf@>=2.2.8 <3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz" + }, + "safe-buffer": { + "version": "5.1.2", + "from": "safe-buffer@>=5.1.1 <5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + }, + "safer-buffer": { + "version": "2.1.2", + "from": "safer-buffer@>=2.1.2 <3.0.0", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + }, + "sandboxed-module": { + "version": "0.3.0", + "from": "sandboxed-module@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", + "dev": true, + "dependencies": { + "stack-trace": { + "version": "0.0.6", + "from": "stack-trace@0.0.6", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "from": "sax@>=1.2.4 <2.0.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" + }, + "semver": { + "version": "5.6.0", + "from": "semver@>=5.5.0 <6.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz" + }, + "send": { + "version": "0.16.2", + "from": "send@0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "dependencies": { + "statuses": { + "version": "1.4.0", + "from": "statuses@>=1.4.0 <1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + } + } + }, + "sequelize": { + "version": "4.42.0", + "from": "sequelize@>=4.38.0 <5.0.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.42.0.tgz", + "dependencies": { + "debug": { + "version": "3.2.6", + "from": "debug@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" + }, + "ms": { + "version": "2.1.1", + "from": "ms@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + }, + "uuid": { + "version": "3.3.2", + "from": "uuid@>=3.2.1 <4.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + } + } + }, + "serve-static": { + "version": "1.13.2", + "from": "serve-static@1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz" + }, + "set-blocking": { + "version": "2.0.0", + "from": "set-blocking@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + }, + "setprototypeof": { + "version": "1.1.0", + "from": "setprototypeof@1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + }, + "settings-sharelatex": { + "version": "1.0.0", + "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", + "dependencies": { + "coffee-script": { + "version": "1.6.0", + "from": "coffee-script@1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" + } + } + }, + "shimmer": { + "version": "1.2.0", + "from": "shimmer@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@~1.0.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "from": "signal-exit@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz" + }, + "sinon": { + "version": "1.7.3", + "from": "sinon@>=1.7.3 <1.8.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", + "dev": true + }, + "smoke-test-sharelatex": { + "version": "1.0.1", + "from": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", + "resolved": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + }, + "source-map": { + "version": "0.6.1", + "from": "source-map@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + }, + "split": { + "version": "1.0.1", + "from": "split@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz" + }, + "split-ca": { + "version": "1.0.1", + "from": "split-ca@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz" + }, + "sqlite3": { + "version": "4.0.4", + "from": "sqlite3@>=4.0.2 <5.0.0", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.4.tgz", + "dependencies": { + "nan": { + "version": "2.10.0", + "from": "nan@>=2.10.0 <2.11.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz" + } + } + }, + "sshpk": { + "version": "1.15.2", + "from": "sshpk@>=1.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz" + }, + "stack-trace": { + "version": "0.0.9", + "from": "stack-trace@0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz" + }, + "statsd-parser": { + "version": "0.0.4", + "from": "statsd-parser@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz" + }, + "statuses": { + "version": "1.5.0", + "from": "statuses@>=1.4.0 <2.0.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + }, + "stream-shift": { + "version": "1.0.0", + "from": "stream-shift@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz" + }, + "string_decoder": { + "version": "1.1.1", + "from": "string_decoder@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + }, + "string-width": { + "version": "1.0.2", + "from": "string-width@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" + }, + "stringstream": { + "version": "0.0.6", + "from": "stringstream@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz" + }, + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + }, + "strip-json-comments": { + "version": "2.0.1", + "from": "strip-json-comments@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + }, + "supports-color": { + "version": "4.4.0", + "from": "supports-color@4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "dev": true + }, + "symbol-observable": { + "version": "1.2.0", + "from": "symbol-observable@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz" + }, + "tar": { + "version": "4.4.8", + "from": "tar@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + } + } + }, + "tar-fs": { + "version": "1.16.3", + "from": "tar-fs@>=1.16.3 <1.17.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + } + } + }, + "tar-pack": { + "version": "3.4.1", + "from": "tar-pack@>=3.4.0 <4.0.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "dependencies": { + "tar": { + "version": "2.2.1", + "from": "tar@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" + } + } + }, + "tar-stream": { + "version": "1.6.2", + "from": "tar-stream@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz" + }, + "tdigest": { + "version": "0.1.1", + "from": "tdigest@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz" + }, + "teeny-request": { + "version": "3.11.3", + "from": "teeny-request@>=3.11.1 <4.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "dependencies": { + "uuid": { + "version": "3.3.2", + "from": "uuid@>=3.3.2 <4.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + } + } + }, + "terraformer": { + "version": "1.0.9", + "from": "terraformer@>=1.0.5 <1.1.0", + "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.9.tgz" + }, + "terraformer-wkt-parser": { + "version": "1.2.0", + "from": "terraformer-wkt-parser@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz" + }, + "through": { + "version": "2.3.8", + "from": "through@>=2.2.7 <3.0.0", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + }, + "through2": { + "version": "2.0.5", + "from": "through2@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" + }, + "timekeeper": { + "version": "0.0.4", + "from": "timekeeper@0.0.4", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", + "dev": true + }, + "to-buffer": { + "version": "1.1.1", + "from": "to-buffer@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz" + }, + "toposort-class": { + "version": "1.0.1", + "from": "toposort-class@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz" + }, + "tough-cookie": { + "version": "2.4.3", + "from": "tough-cookie@>=2.4.3 <2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "dependencies": { + "punycode": { + "version": "1.4.1", + "from": "punycode@>=1.4.1 <2.0.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "from": "tunnel-agent@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + }, + "tweetnacl": { + "version": "0.14.5", + "from": "tweetnacl@>=0.14.0 <0.15.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + }, + "type-detect": { + "version": "0.1.1", + "from": "type-detect@0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "from": "type-is@>=1.6.16 <1.7.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz" + }, + "typedarray": { + "version": "0.0.6", + "from": "typedarray@>=0.0.6 <0.0.7", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" + }, + "uid-number": { + "version": "0.0.6", + "from": "uid-number@>=0.0.6 <0.0.7", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" + }, + "underscore": { + "version": "1.9.1", + "from": "underscore@>=1.8.2 <2.0.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz" + }, + "underscore.string": { + "version": "2.2.1", + "from": "underscore.string@>=2.2.1 <2.3.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "from": "unpipe@1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + }, + "uri-js": { + "version": "4.2.2", + "from": "uri-js@>=4.2.2 <5.0.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz" + }, + "util-deprecate": { + "version": "1.0.2", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + }, + "utils-merge": { + "version": "1.0.1", + "from": "utils-merge@1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + }, + "uuid": { + "version": "3.0.0", + "from": "uuid@3.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz" + }, + "v8-profiler": { + "version": "5.7.0", + "from": "v8-profiler@>=5.2.4 <6.0.0", + "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", + "dependencies": { + "ajv": { + "version": "4.11.8", + "from": "ajv@>=4.9.1 <5.0.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz" + }, + "assert-plus": { + "version": "0.2.0", + "from": "assert-plus@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" + }, + "aws-sign2": { + "version": "0.6.0", + "from": "aws-sign2@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" + }, + "form-data": { + "version": "2.1.4", + "from": "form-data@>=2.1.1 <2.2.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz" + }, + "har-schema": { + "version": "1.0.5", + "from": "har-schema@>=1.0.5 <2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz" + }, + "har-validator": { + "version": "4.2.1", + "from": "har-validator@>=4.2.1 <4.3.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz" + }, + "http-signature": { + "version": "1.1.1", + "from": "http-signature@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + }, + "node-pre-gyp": { + "version": "0.6.39", + "from": "node-pre-gyp@>=0.6.34 <0.7.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz" + }, + "oauth-sign": { + "version": "0.8.2", + "from": "oauth-sign@>=0.8.1 <0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" + }, + "performance-now": { + "version": "0.2.0", + "from": "performance-now@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz" + }, + "punycode": { + "version": "1.4.1", + "from": "punycode@>=1.4.1 <2.0.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + }, + "qs": { + "version": "6.4.0", + "from": "qs@>=6.4.0 <6.5.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz" + }, + "request": { + "version": "2.81.0", + "from": "request@2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz" + }, + "tar": { + "version": "2.2.1", + "from": "tar@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" + }, + "tough-cookie": { + "version": "2.3.4", + "from": "tough-cookie@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz" + } + } + }, + "validator": { + "version": "10.9.0", + "from": "validator@>=10.4.0 <11.0.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.9.0.tgz" + }, + "vary": { + "version": "1.1.2", + "from": "vary@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + }, + "verror": { + "version": "1.10.0", + "from": "verror@1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + }, + "which": { + "version": "1.0.9", + "from": "which@>=1.0.5 <1.1.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "from": "wide-align@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz" + }, + "wkx": { + "version": "0.4.6", + "from": "wkx@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.6.tgz" + }, + "wrappy": { + "version": "1.0.2", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + }, + "wrench": { + "version": "1.5.9", + "from": "wrench@>=1.5.4 <1.6.0", + "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + }, + "yallist": { + "version": "3.0.3", + "from": "yallist@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz" + } + } +} From 541dac11cbd70b0ffa4d220cfcd15a8904e420cb Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 8 Jan 2019 12:56:16 +0000 Subject: [PATCH 428/709] pull clsi compile size limit into setting and bump to 7mb --- app.coffee | 4 ++-- config/settings.defaults.coffee | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app.coffee b/app.coffee index 10bceee5..f81021e1 100644 --- a/app.coffee +++ b/app.coffee @@ -58,7 +58,7 @@ app.param 'build_id', (req, res, next, build_id) -> next new Error("invalid build id #{build_id}") -app.post "/project/:project_id/compile", bodyParser.json(limit: "5mb"), CompileController.compile +app.post "/project/:project_id/compile", bodyParser.json(limit: Settings.compileSizeLimit), CompileController.compile app.post "/project/:project_id/compile/stop", CompileController.stopCompile app.delete "/project/:project_id", CompileController.clearCache @@ -68,7 +68,7 @@ app.get "/project/:project_id/wordcount", CompileController.wordcount 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", bodyParser.json(limit: Settings.compileSizeLimit), CompileController.compile app.post "/project/:project_id/user/:user_id/compile/stop", CompileController.stopCompile app.delete "/project/:project_id/user/:user_id", CompileController.clearCache diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 88f4532f..ad3f04d8 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -15,6 +15,8 @@ module.exports = retry: max: 10 + compileSizeLimit: process.env["COMPILE_SIZE_LIMIT"] or "7mb" + path: compilesDir: Path.resolve(__dirname + "/../compiles") clsiCacheDir: Path.resolve(__dirname + "/../cache") @@ -32,6 +34,7 @@ module.exports = apis: clsi: url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" + smokeTest: process.env["SMOKE_TEST"] or false project_cache_length_ms: 1000 * 60 * 60 * 24 From b52a8b2aa2131423c3d62d690343aa7f3f542b83 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 11 Jan 2019 10:18:37 +0000 Subject: [PATCH 429/709] Bump logger to v1.5.9 and settings to v1.1.0 --- npm-shrinkwrap.json | 471 +++++++++++++++++++++++++++++++++----------- package.json | 4 +- 2 files changed, 360 insertions(+), 115 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4853442c..48e31b11 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -118,6 +118,28 @@ "from": "@sindresorhus/is@>=0.13.0 <0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.13.0.tgz" }, + "@sinonjs/commons": { + "version": "1.3.0", + "from": "@sinonjs/commons@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz", + "dependencies": { + "type-detect": { + "version": "4.0.8", + "from": "type-detect@4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + } + } + }, + "@sinonjs/formatio": { + "version": "3.1.0", + "from": "@sinonjs/formatio@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.1.0.tgz" + }, + "@sinonjs/samsam": { + "version": "3.0.2", + "from": "@sinonjs/samsam@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.0.2.tgz" + }, "@types/caseless": { "version": "0.12.1", "from": "@types/caseless@*", @@ -218,19 +240,16 @@ "version": "0.1.16", "from": "argparse@>=0.1.11 <0.2.0", "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "dev": true, "dependencies": { "underscore": { "version": "1.7.0", "from": "underscore@>=1.7.0 <1.8.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz" }, "underscore.string": { "version": "2.4.0", "from": "underscore.string@>=2.4.0 <2.5.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz" } } }, @@ -239,6 +258,11 @@ "from": "array-flatten@1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" }, + "array-from": { + "version": "2.1.1", + "from": "array-from@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz" + }, "arrify": { "version": "1.0.1", "from": "arrify@>=1.0.1 <2.0.0", @@ -421,6 +445,11 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", "dev": true }, + "check-error": { + "version": "1.0.2", + "from": "check-error@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" + }, "chownr": { "version": "1.1.1", "from": "chownr@>=1.0.1 <2.0.0", @@ -444,14 +473,12 @@ "coffee-script": { "version": "1.6.0", "from": "coffee-script@1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" }, "colors": { "version": "0.6.2", "from": "colors@>=0.6.2 <0.7.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz" }, "combined-stream": { "version": "1.0.7", @@ -461,8 +488,7 @@ "commander": { "version": "2.0.0", "from": "commander@2.0.0", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "dev": true + "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz" }, "concat-map": { "version": "0.0.1", @@ -527,8 +553,7 @@ "dateformat": { "version": "1.0.2-1.2.3", "from": "dateformat@1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz" }, "debug": { "version": "2.6.9", @@ -579,8 +604,7 @@ "diff": { "version": "1.0.7", "from": "diff@1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz" }, "docker-modem": { "version": "1.0.7", @@ -689,14 +713,12 @@ "escape-string-regexp": { "version": "1.0.5", "from": "escape-string-regexp@1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" }, "esprima": { "version": "1.0.4", "from": "esprima@>=1.0.2 <1.1.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" }, "etag": { "version": "1.8.1", @@ -706,14 +728,12 @@ "eventemitter2": { "version": "0.4.14", "from": "eventemitter2@>=0.4.13 <0.5.0", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" }, "exit": { "version": "0.1.2", "from": "exit@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" }, "express": { "version": "4.16.4", @@ -768,31 +788,26 @@ "version": "0.1.3", "from": "findup-sync@>=0.1.2 <0.2.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "dev": true, "dependencies": { "glob": { "version": "3.2.11", "from": "glob@>=3.2.9 <3.3.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz" }, "lodash": { "version": "2.4.2", "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" }, "lru-cache": { "version": "2.7.3", "from": "lru-cache@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "minimatch": { "version": "0.3.0", "from": "minimatch@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz" } } }, @@ -890,11 +905,15 @@ "from": "generic-pool@>=3.4.0 <4.0.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz" }, + "get-func-name": { + "version": "2.0.0", + "from": "get-func-name@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" + }, "getobject": { "version": "0.1.0", "from": "getobject@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz" }, "getpass": { "version": "0.1.7", @@ -931,80 +950,67 @@ "growl": { "version": "1.7.0", "from": "growl@1.7.x", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz" }, "grunt": { "version": "0.4.5", "from": "grunt@>=0.4.2 <0.5.0", "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", - "dev": true, "dependencies": { "async": { "version": "0.1.22", "from": "async@>=0.1.22 <0.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" }, "coffee-script": { "version": "1.3.3", "from": "coffee-script@>=1.3.3 <1.4.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz" }, "glob": { "version": "3.1.21", "from": "glob@>=3.1.21 <3.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz" }, "graceful-fs": { "version": "1.2.3", "from": "graceful-fs@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz" }, "iconv-lite": { "version": "0.2.11", "from": "iconv-lite@>=0.2.11 <0.3.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" }, "inherits": { "version": "1.0.2", "from": "inherits@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz" }, "lodash": { "version": "0.9.2", "from": "lodash@>=0.9.2 <0.10.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" }, "lru-cache": { "version": "2.7.3", "from": "lru-cache@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "minimatch": { "version": "0.2.14", "from": "minimatch@>=0.2.12 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" }, "nopt": { "version": "1.0.10", "from": "nopt@>=1.0.10 <1.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" }, "rimraf": { "version": "2.2.8", "from": "rimraf@>=2.2.8 <2.3.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" } } }, @@ -1012,13 +1018,11 @@ "version": "0.5.0", "from": "grunt-bunyan@>=0.5.0 <0.6.0", "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", - "dev": true, "dependencies": { "lodash": { "version": "2.4.2", "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" } } }, @@ -1060,19 +1064,16 @@ "version": "0.1.3", "from": "grunt-legacy-log@>=0.1.0 <0.2.0", "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "dev": true, "dependencies": { "lodash": { "version": "2.4.2", "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" }, "underscore.string": { "version": "2.3.3", "from": "underscore.string@>=2.3.3 <2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" } } }, @@ -1080,19 +1081,16 @@ "version": "0.1.1", "from": "grunt-legacy-log-utils@>=0.1.1 <0.2.0", "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "dev": true, "dependencies": { "lodash": { "version": "2.4.2", "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" }, "underscore.string": { "version": "2.3.3", "from": "underscore.string@>=2.3.3 <2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" } } }, @@ -1100,19 +1098,16 @@ "version": "0.2.0", "from": "grunt-legacy-util@>=0.2.0 <0.3.0", "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "dev": true, "dependencies": { "async": { "version": "0.1.22", "from": "async@>=0.1.22 <0.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" }, "lodash": { "version": "0.9.2", "from": "lodash@>=0.9.2 <0.10.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" } } }, @@ -1192,6 +1187,18 @@ "from": "har-validator@>=5.1.0 <5.2.0", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz" }, + "has-ansi": { + "version": "0.1.0", + "from": "has-ansi@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "from": "ansi-regex@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" + } + } + }, "has-color": { "version": "0.1.7", "from": "has-color@>=0.1.0 <0.2.0", @@ -1238,8 +1245,7 @@ "hooker": { "version": "0.2.3", "from": "hooker@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" }, "http-errors": { "version": "1.6.3", @@ -1342,27 +1348,23 @@ "version": "0.26.3", "from": "jade@0.26.3", "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "dev": true, "dependencies": { "commander": { "version": "0.6.1", "from": "commander@0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" }, "mkdirp": { "version": "0.3.0", "from": "mkdirp@0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" } } }, "js-yaml": { "version": "2.0.5", "from": "js-yaml@>=2.0.5 <2.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz" }, "jsbn": { "version": "0.1.1", @@ -1427,6 +1429,11 @@ "from": "jsprim@>=1.2.2 <2.0.0", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz" }, + "just-extend": { + "version": "4.0.2", + "from": "just-extend@>=4.0.2 <5.0.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz" + }, "jwa": { "version": "1.1.6", "from": "jwa@>=1.1.5 <2.0.0", @@ -1447,15 +1454,204 @@ "from": "lodash@>=4.17.1 <5.0.0", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz" }, + "lodash.get": { + "version": "4.4.2", + "from": "lodash.get@>=4.4.2 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" + }, "lodash.pickby": { "version": "4.6.0", "from": "lodash.pickby@>=4.6.0 <5.0.0", "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz" }, "logger-sharelatex": { - "version": "1.5.4", - "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", - "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#9ee7b52eb2bbd8fcbb1e2c708587c1e93fd4c733" + "version": "1.5.9", + "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.9", + "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#e8e1b95052f62e107336053e4a983f81cdbdf589", + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "from": "ansi-regex@^0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" + }, + "ansi-styles": { + "version": "1.1.0", + "from": "ansi-styles@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz" + }, + "assertion-error": { + "version": "1.1.0", + "from": "assertion-error@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" + }, + "bunyan": { + "version": "1.5.1", + "from": "bunyan@1.5.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz" + }, + "chai": { + "version": "4.2.0", + "from": "chai@latest", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz" + }, + "chalk": { + "version": "0.5.1", + "from": "chalk@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz" + }, + "coffee-script": { + "version": "1.12.4", + "from": "coffee-script@1.12.4", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.4.tgz" + }, + "deep-eql": { + "version": "3.0.1", + "from": "deep-eql@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" + }, + "dtrace-provider": { + "version": "0.6.0", + "from": "dtrace-provider@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", + "optional": true + }, + "fs-extra": { + "version": "0.9.1", + "from": "fs-extra@>=0.9.1 <0.10.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.9.1.tgz", + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + } + } + }, + "glob": { + "version": "3.2.3", + "from": "glob@3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz" + }, + "graceful-fs": { + "version": "2.0.3", + "from": "graceful-fs@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + }, + "grunt-contrib-clean": { + "version": "0.6.0", + "from": "grunt-contrib-clean@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.6.0.tgz" + }, + "grunt-contrib-coffee": { + "version": "0.11.1", + "from": "grunt-contrib-coffee@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", + "dependencies": { + "coffee-script": { + "version": "1.7.1", + "from": "coffee-script@>=1.7.0 <1.8.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz" + } + } + }, + "grunt-execute": { + "version": "0.2.2", + "from": "grunt-execute@>=0.2.2 <0.3.0", + "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz" + }, + "grunt-mocha-test": { + "version": "0.11.0", + "from": "grunt-mocha-test@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.11.0.tgz" + }, + "has-flag": { + "version": "3.0.0", + "from": "has-flag@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + }, + "jsonfile": { + "version": "1.1.1", + "from": "jsonfile@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.1.1.tgz" + }, + "lodash": { + "version": "2.4.2", + "from": "lodash@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + }, + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@>=0.2.11 <0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + }, + "mocha": { + "version": "1.20.1", + "from": "mocha@>=1.20.0 <1.21.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.20.1.tgz" + }, + "ncp": { + "version": "0.5.1", + "from": "ncp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz" + }, + "rimraf": { + "version": "2.2.8", + "from": "rimraf@>=2.2.1 <2.3.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" + }, + "sandboxed-module": { + "version": "2.0.3", + "from": "sandboxed-module@latest", + "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-2.0.3.tgz" + }, + "sinon": { + "version": "7.2.2", + "from": "sinon@latest", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz", + "dependencies": { + "diff": { + "version": "3.5.0", + "from": "diff@>=3.5.0 <4.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" + }, + "supports-color": { + "version": "5.5.0", + "from": "supports-color@>=5.5.0 <6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + } + } + }, + "strip-ansi": { + "version": "0.3.0", + "from": "strip-ansi@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz" + }, + "supports-color": { + "version": "0.2.0", + "from": "supports-color@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz" + }, + "timekeeper": { + "version": "1.0.0", + "from": "timekeeper@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-1.0.0.tgz" + }, + "type-detect": { + "version": "4.0.8", + "from": "type-detect@>=4.0.5 <5.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + } + } + }, + "lolex": { + "version": "3.0.0", + "from": "lolex@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz" }, "long": { "version": "4.0.0", @@ -1627,28 +1823,24 @@ "version": "2.1.1", "from": "mv@~2", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "dev": true, "optional": true, "dependencies": { "glob": { "version": "6.0.4", "from": "glob@>=6.0.1 <7.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "dev": true, "optional": true }, "mkdirp": { "version": "0.5.1", "from": "mkdirp@>=0.5.1 <0.6.0", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "dev": true, "optional": true }, "rimraf": { "version": "2.4.5", "from": "rimraf@>=2.4.0 <2.5.0", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "dev": true, "optional": true } } @@ -1694,7 +1886,6 @@ "version": "2.0.0", "from": "ncp@~2.0.0", "resolved": "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "dev": true, "optional": true }, "needle": { @@ -1707,6 +1898,28 @@ "from": "negotiator@0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" }, + "nise": { + "version": "1.4.8", + "from": "nise@>=1.4.7 <2.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.8.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "lolex": { + "version": "2.7.5", + "from": "lolex@>=2.3.2 <3.0.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz" + }, + "path-to-regexp": { + "version": "1.7.0", + "from": "path-to-regexp@>=1.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz" + } + } + }, "node-fetch": { "version": "2.3.0", "from": "node-fetch@>=2.2.0 <3.0.0", @@ -1829,6 +2042,11 @@ "from": "path-to-regexp@0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" }, + "pathval": { + "version": "1.1.0", + "from": "pathval@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz" + }, "performance-now": { "version": "2.1.0", "from": "performance-now@>=2.1.0 <3.0.0", @@ -1941,8 +2159,7 @@ "require-like": { "version": "0.1.2", "from": "require-like@0.1.2", - "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz" }, "resolve": { "version": "1.9.0", @@ -1974,6 +2191,12 @@ "from": "safe-buffer@>=5.1.1 <5.2.0", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" }, + "safe-json-stringify": { + "version": "1.2.0", + "from": "safe-json-stringify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "optional": true + }, "safer-buffer": { "version": "2.1.2", "from": "safer-buffer@>=2.1.2 <3.0.0", @@ -2053,16 +2276,9 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" }, "settings-sharelatex": { - "version": "1.0.0", - "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", - "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", - "dependencies": { - "coffee-script": { - "version": "1.6.0", - "from": "coffee-script@1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" - } - } + "version": "1.1.0", + "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", + "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#93f63d029b52fef8825c3a401b2b6a7ba29b4750" }, "shimmer": { "version": "1.2.0", @@ -2072,8 +2288,7 @@ "sigmund": { "version": "1.0.1", "from": "sigmund@~1.0.0", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" }, "signal-exit": { "version": "3.0.2", @@ -2089,7 +2304,34 @@ "smoke-test-sharelatex": { "version": "1.0.1", "from": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "resolved": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c" + "resolved": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", + "dependencies": { + "glob": { + "version": "3.2.3", + "from": "glob@3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz" + }, + "graceful-fs": { + "version": "2.0.3", + "from": "graceful-fs@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + }, + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@>=0.2.11 <0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + }, + "mocha": { + "version": "1.17.1", + "from": "mocha@>=1.17.0 <1.18.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz" + } + } }, "sntp": { "version": "1.0.9", @@ -2252,6 +2494,11 @@ "from": "terraformer-wkt-parser@>=1.1.2 <2.0.0", "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz" }, + "text-encoding": { + "version": "0.6.4", + "from": "text-encoding@>=0.6.4 <0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz" + }, "through": { "version": "2.3.8", "from": "through@>=2.2.7 <3.0.0", @@ -2329,8 +2576,7 @@ "underscore.string": { "version": "2.2.1", "from": "underscore.string@>=2.2.1 <2.3.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz" }, "unpipe": { "version": "1.0.0", @@ -2462,8 +2708,7 @@ "which": { "version": "1.0.9", "from": "which@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" }, "wide-align": { "version": "1.1.3", diff --git a/package.json b/package.json index 49f5873e..f981e5fa 100644 --- a/package.json +++ b/package.json @@ -29,14 +29,14 @@ "grunt-mkdir": "^1.0.0", "heapdump": "^0.3.5", "lockfile": "^1.0.3", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.9", "lynx": "0.0.11", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.12", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", "sequelize": "^4.38.0", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", "sqlite3": "^4.0.2", "underscore": "^1.8.2", From 5d2eb129e8d7176b84ff756205002f94bcf8df8d Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 11 Jan 2019 10:19:47 +0000 Subject: [PATCH 430/709] Init metrics at top of app.coffee --- app.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app.coffee b/app.coffee index f81021e1..ec9fc803 100644 --- a/app.coffee +++ b/app.coffee @@ -1,3 +1,6 @@ +Metrics = require "metrics-sharelatex" +Metrics.initialize("clsi") + CompileController = require "./app/js/CompileController" Settings = require "settings-sharelatex" logger = require "logger-sharelatex" @@ -12,8 +15,7 @@ Errors = require './app/js/Errors' Path = require "path" fs = require "fs" -Metrics = require "metrics-sharelatex" -Metrics.initialize("clsi") + Metrics.open_sockets.monitor(logger) Metrics.memory.monitor(logger) From d4e9aca9e2d4e78ab9f1e60ddfde5476f12149ea Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 11 Jan 2019 11:52:10 +0000 Subject: [PATCH 431/709] Bump buildscript to 1.1.11 --- .github/ISSUE_TEMPLATE.md | 38 +++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 45 ++++++++++++++++++++++++++++++++ Dockerfile | 2 +- Makefile | 2 +- buildscript.txt | 9 +++++++ docker-compose.ci.yml | 3 ++- docker-compose.yml | 2 +- package.json | 2 +- 8 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 buildscript.txt diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..e0093aa9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,38 @@ +<!-- BUG REPORT TEMPLATE --> + +## Steps to Reproduce +<!-- Describe the steps leading up to when / where you found the bug. --> +<!-- Screenshots may be helpful here. --> + +1. +2. +3. + +## Expected Behaviour +<!-- What should have happened when you completed the steps above? --> + +## Observed Behaviour +<!-- What actually happened when you completed the steps above? --> +<!-- Screenshots may be helpful here. --> + +## Context +<!-- How has this issue affected you? What were you trying to accomplish? --> + +## Technical Info +<!-- Provide any technical details that may be applicable (or N/A if not applicable). --> + +* URL: +* Browser Name and version: +* Operating System and version (desktop or mobile): +* Signed in as: +* Project and/or file: + +## Analysis +<!--- Optionally, document investigation of / suggest a fix for the bug, e.g. 'comes from this line / commit' --> + +## Who Needs to Know? +<!-- If you want to bring this to the attention of particular people, @-mention them below. --> +<!-- If a user reported this bug and should be notified when it is fixed, provide the Front conversation link. --> + +- +- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..ed25ee83 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ +<!-- Please review https://github.com/overleaf/write_latex/blob/master/.github/CONTRIBUTING.md for guidance on what is expected in each section. --> + +### Description + + + +#### Screenshots + + + +#### Related Issues / PRs + + + +### Review + + + +#### Potential Impact + + + +#### Manual Testing Performed + +- [ ] +- [ ] + +#### Accessibility + + + +### Deployment + + + +#### Deployment Checklist + +- [ ] Update documentation not included in the PR (if any) +- [ ] + +#### Metrics and Monitoring + + + +#### Who Needs to Know? diff --git a/Dockerfile b/Dockerfile index ea550b28..861da2a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,4 @@ WORKDIR /app RUN chmod 0755 ./install_deps.sh && ./install_deps.sh ENTRYPOINT ["/bin/sh", "entrypoint.sh"] -CMD ["node","app.js"] +CMD ["node", "--expose-gc", "app.js"] diff --git a/Makefile b/Makefile index b1ce2934..e7a1e341 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.9 +# Version: 1.1.11 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/buildscript.txt b/buildscript.txt new file mode 100644 index 00000000..abdf7ed9 --- /dev/null +++ b/buildscript.txt @@ -0,0 +1,9 @@ +--script-version=1.1.11 +clsi +--node-version=6.15.1 +--acceptance-creds=None +--language=coffeescript +--dependencies=mongo,redis +--docker-repos=gcr.io/overleaf-ops +--kube=false +--build-target=docker diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index f6c8a27f..698c5cb1 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.9 +# Version: 1.1.11 version: "2" @@ -10,6 +10,7 @@ services: image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER command: npm run test:unit:_run + test_acceptance: build: . image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER diff --git a/docker-compose.yml b/docker-compose.yml index 371e6e76..5c3c4c86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.9 +# Version: 1.1.11 version: "2" diff --git a/package.json b/package.json index f981e5fa..a0e0a43b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/sharelatex/clsi-sharelatex.git" }, "scripts": { - "compile:app": "([ -e app/coffee ] && coffee $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", + "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", From 2fceac6ac8d5c2e2cdcf716f8d9ea469253c0c88 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 11 Jan 2019 12:06:45 +0000 Subject: [PATCH 432/709] Remove grunt --- Gruntfile.coffee | 104 ---------------- npm-shrinkwrap.json | 280 +++++++++++++++----------------------------- package.json | 18 +-- 3 files changed, 99 insertions(+), 303 deletions(-) delete mode 100644 Gruntfile.coffee diff --git a/Gruntfile.coffee b/Gruntfile.coffee deleted file mode 100644 index a30f141e..00000000 --- a/Gruntfile.coffee +++ /dev/null @@ -1,104 +0,0 @@ -spawn = require("child_process").spawn - -module.exports = (grunt) -> - grunt.initConfig - coffee: - app_src: - expand: true, - flatten: true, - cwd: "app" - src: ['coffee/*.coffee'], - dest: 'app/js/', - ext: '.js' - - app: - src: "app.coffee" - dest: "app.js" - - unit_tests: - expand: true - cwd: "test/unit/coffee" - src: ["**/*.coffee"] - dest: "test/unit/js/" - ext: ".js" - - acceptance_tests: - expand: true - cwd: "test/acceptance/coffee" - src: ["**/*.coffee"] - dest: "test/acceptance/js/" - ext: ".js" - - smoke_tests: - expand: true - cwd: "test/smoke/coffee" - src: ["**/*.coffee"] - dest: "test/smoke/js" - ext: ".js" - - clean: - app: ["app/js/"] - unit_tests: ["test/unit/js"] - acceptance_tests: ["test/acceptance/js"] - smoke_tests: ["test/smoke/js"] - - execute: - app: - src: "app.js" - - mkdir: - all: - options: - create: ["cache", "compiles"] - - mochaTest: - unit: - options: - reporter: "spec" - grep: grunt.option("grep") - src: ["test/unit/js/**/*.js"] - acceptance: - options: - reporter: "spec" - timeout: 40000 - grep: grunt.option("grep") - src: ["test/acceptance/js/**/*.js"] - smoke: - options: - reported: "spec" - timeout: 10000 - src: ["test/smoke/js/**/*.js"] - - grunt.loadNpmTasks 'grunt-contrib-coffee' - grunt.loadNpmTasks 'grunt-contrib-clean' - grunt.loadNpmTasks 'grunt-mocha-test' - grunt.loadNpmTasks 'grunt-shell' - grunt.loadNpmTasks 'grunt-execute' - grunt.loadNpmTasks 'grunt-bunyan' - grunt.loadNpmTasks 'grunt-mkdir' - - grunt.registerTask 'compile:bin', () -> - callback = @async() - proc = spawn "cc", [ - "-o", "bin/synctex", "-Isrc/synctex", - "src/synctex.c", "src/synctex/synctex_parser.c", "src/synctex/synctex_parser_utils.c", "-lz" - ], stdio: "inherit" - proc.on "close", callback - - grunt.registerTask 'compile:app', ['clean:app', 'coffee:app', 'coffee:app_src', 'coffee:smoke_tests', 'compile:bin'] - grunt.registerTask 'run', ['compile:app', 'bunyan', 'execute'] - - grunt.registerTask 'compile:unit_tests', ['clean:unit_tests', 'coffee:unit_tests'] - grunt.registerTask 'test:unit', ['compile:app', 'compile:unit_tests', 'mochaTest:unit'] - - grunt.registerTask 'compile:acceptance_tests', ['clean:acceptance_tests', 'coffee:acceptance_tests'] - grunt.registerTask 'test:acceptance', ['compile:acceptance_tests', 'mochaTest:acceptance'] - - grunt.registerTask 'compile:smoke_tests', ['clean:smoke_tests', 'coffee:smoke_tests'] - grunt.registerTask 'test:smoke', ['compile:smoke_tests', 'mochaTest:smoke'] - - grunt.registerTask 'install', 'compile:app' - - grunt.registerTask 'default', ['mkdir', 'run'] - - diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 48e31b11..83b6b503 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -221,10 +221,9 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" }, "ansi-styles": { - "version": "0.2.0", - "from": "ansi-styles@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", - "dev": true + "version": "1.1.0", + "from": "ansi-styles@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz" }, "aproba": { "version": "1.2.0", @@ -440,10 +439,26 @@ "dev": true }, "chalk": { - "version": "0.3.0", - "from": "chalk@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.3.0.tgz", - "dev": true + "version": "0.5.1", + "from": "chalk@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "from": "ansi-regex@^0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" + }, + "strip-ansi": { + "version": "0.3.0", + "from": "strip-ansi@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz" + }, + "supports-color": { + "version": "0.2.0", + "from": "supports-color@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz" + } + } }, "check-error": { "version": "1.0.2", @@ -954,7 +969,7 @@ }, "grunt": { "version": "0.4.5", - "from": "grunt@>=0.4.2 <0.5.0", + "from": "grunt@>=0.4.5 <0.5.0", "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", "dependencies": { "async": { @@ -994,7 +1009,7 @@ }, "lru-cache": { "version": "2.7.3", - "from": "lru-cache@>=2.0.0 <3.0.0", + "from": "lru-cache@2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "minimatch": { @@ -1027,38 +1042,38 @@ } }, "grunt-contrib-clean": { - "version": "0.5.0", - "from": "grunt-contrib-clean@>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", - "dev": true, + "version": "0.6.0", + "from": "grunt-contrib-clean@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.6.0.tgz", "dependencies": { "rimraf": { "version": "2.2.8", - "from": "rimraf@>=2.2.1 <2.3.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "dev": true + "from": "rimraf@~2.2.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" } } }, "grunt-contrib-coffee": { - "version": "0.7.0", - "from": "grunt-contrib-coffee@>=0.7.0 <0.8.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.7.0.tgz", - "dev": true, + "version": "0.11.1", + "from": "grunt-contrib-coffee@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", "dependencies": { "coffee-script": { - "version": "1.6.3", - "from": "coffee-script@>=1.6.2 <1.7.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", - "dev": true + "version": "1.7.1", + "from": "coffee-script@>=1.7.0 <1.8.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz" + }, + "lodash": { + "version": "2.4.2", + "from": "lodash@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" } } }, "grunt-execute": { - "version": "0.1.5", - "from": "grunt-execute@>=0.1.5 <0.2.0", - "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.1.5.tgz", - "dev": true + "version": "0.2.2", + "from": "grunt-execute@>=0.2.2 <0.3.0", + "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz" }, "grunt-legacy-log": { "version": "0.1.3", @@ -1101,65 +1116,75 @@ "dependencies": { "async": { "version": "0.1.22", - "from": "async@>=0.1.22 <0.2.0", + "from": "async@~0.1.22", "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" }, "lodash": { "version": "0.9.2", - "from": "lodash@>=0.9.2 <0.10.0", + "from": "lodash@~0.9.2", "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" } } }, - "grunt-mkdir": { - "version": "1.0.0", - "from": "grunt-mkdir@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/grunt-mkdir/-/grunt-mkdir-1.0.0.tgz" - }, "grunt-mocha-test": { - "version": "0.8.2", - "from": "grunt-mocha-test@>=0.8.1 <0.9.0", - "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.8.2.tgz", - "dev": true, + "version": "0.11.0", + "from": "grunt-mocha-test@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.11.0.tgz", "dependencies": { + "fs-extra": { + "version": "0.9.1", + "from": "fs-extra@>=0.9.1 <0.10.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.9.1.tgz" + }, "glob": { "version": "3.2.3", "from": "glob@3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz" }, "graceful-fs": { "version": "2.0.3", "from": "graceful-fs@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "dev": true + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + }, + "jsonfile": { + "version": "1.1.1", + "from": "jsonfile@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.1.1.tgz" }, "lru-cache": { "version": "2.7.3", - "from": "lru-cache@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "dev": true + "from": "lru-cache@2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "minimatch": { "version": "0.2.14", - "from": "minimatch@>=0.2.11 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "dev": true + "from": "minimatch@~0.2.11", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" }, "mocha": { - "version": "1.14.0", - "from": "mocha@>=1.14.0 <1.15.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.14.0.tgz", - "dev": true + "version": "1.20.1", + "from": "mocha@>=1.20.0 <1.21.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.20.1.tgz", + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + } + } + }, + "ncp": { + "version": "0.5.1", + "from": "ncp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz" } } }, - "grunt-shell": { - "version": "0.6.4", - "from": "grunt-shell@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-0.6.4.tgz", - "dev": true - }, "gtoken": { "version": "2.3.0", "from": "gtoken@>=2.3.0 <3.0.0", @@ -1199,12 +1224,6 @@ } } }, - "has-color": { - "version": "0.1.7", - "from": "has-color@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "dev": true - }, "has-flag": { "version": "2.0.0", "from": "has-flag@>=2.0.0 <3.0.0", @@ -1469,16 +1488,6 @@ "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.9", "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#e8e1b95052f62e107336053e4a983f81cdbdf589", "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "from": "ansi-regex@^0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" - }, - "ansi-styles": { - "version": "1.1.0", - "from": "ansi-styles@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz" - }, "assertion-error": { "version": "1.1.0", "from": "assertion-error@>=1.1.0 <2.0.0", @@ -1494,11 +1503,6 @@ "from": "chai@latest", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz" }, - "chalk": { - "version": "0.5.1", - "from": "chalk@>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz" - }, "coffee-script": { "version": "1.12.4", "from": "coffee-script@1.12.4", @@ -1509,101 +1513,22 @@ "from": "deep-eql@>=3.0.1 <4.0.0", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" }, + "diff": { + "version": "3.5.0", + "from": "diff@>=3.5.0 <4.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" + }, "dtrace-provider": { "version": "0.6.0", "from": "dtrace-provider@>=0.6.0 <0.7.0", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", "optional": true }, - "fs-extra": { - "version": "0.9.1", - "from": "fs-extra@>=0.9.1 <0.10.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.9.1.tgz", - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "from": "mkdirp@>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - } - } - }, - "glob": { - "version": "3.2.3", - "from": "glob@3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz" - }, - "graceful-fs": { - "version": "2.0.3", - "from": "graceful-fs@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" - }, - "grunt-contrib-clean": { - "version": "0.6.0", - "from": "grunt-contrib-clean@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.6.0.tgz" - }, - "grunt-contrib-coffee": { - "version": "0.11.1", - "from": "grunt-contrib-coffee@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", - "dependencies": { - "coffee-script": { - "version": "1.7.1", - "from": "coffee-script@>=1.7.0 <1.8.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz" - } - } - }, - "grunt-execute": { - "version": "0.2.2", - "from": "grunt-execute@>=0.2.2 <0.3.0", - "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz" - }, - "grunt-mocha-test": { - "version": "0.11.0", - "from": "grunt-mocha-test@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.11.0.tgz" - }, "has-flag": { "version": "3.0.0", "from": "has-flag@>=3.0.0 <4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" }, - "jsonfile": { - "version": "1.1.1", - "from": "jsonfile@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.1.1.tgz" - }, - "lodash": { - "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" - }, - "lru-cache": { - "version": "2.7.3", - "from": "lru-cache@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" - }, - "minimatch": { - "version": "0.2.14", - "from": "minimatch@>=0.2.11 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" - }, - "mocha": { - "version": "1.20.1", - "from": "mocha@>=1.20.0 <1.21.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.20.1.tgz" - }, - "ncp": { - "version": "0.5.1", - "from": "ncp@>=0.5.1 <0.6.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz" - }, - "rimraf": { - "version": "2.2.8", - "from": "rimraf@>=2.2.1 <2.3.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" - }, "sandboxed-module": { "version": "2.0.3", "from": "sandboxed-module@latest", @@ -1612,29 +1537,12 @@ "sinon": { "version": "7.2.2", "from": "sinon@latest", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz", - "dependencies": { - "diff": { - "version": "3.5.0", - "from": "diff@>=3.5.0 <4.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" - }, - "supports-color": { - "version": "5.5.0", - "from": "supports-color@>=5.5.0 <6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - } - } - }, - "strip-ansi": { - "version": "0.3.0", - "from": "strip-ansi@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz" + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz" }, "supports-color": { - "version": "0.2.0", - "from": "supports-color@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz" + "version": "5.5.0", + "from": "supports-color@>=5.5.0 <6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" }, "timekeeper": { "version": "1.0.0", diff --git a/package.json b/package.json index a0e0a43b..065cdce6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "dockerode": "^2.5.3", "express": "^4.2.0", "fs-extra": "^0.16.3", - "grunt-mkdir": "^1.0.0", "heapdump": "^0.3.5", "lockfile": "^1.0.3", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.9", @@ -44,19 +43,12 @@ "wrench": "~1.5.4" }, "devDependencies": { - "mocha": "^4.0.1", - "coffee-script": "1.6.0", + "bunyan": "^0.22.1", "chai": "~1.8.1", - "sinon": "~1.7.3", - "grunt": "~0.4.2", - "grunt-contrib-coffee": "~0.7.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-shell": "~0.6.1", - "grunt-mocha-test": "~0.8.1", + "coffee-script": "1.6.0", + "mocha": "^4.0.1", "sandboxed-module": "~0.3.0", - "timekeeper": "0.0.4", - "grunt-execute": "^0.1.5", - "bunyan": "^0.22.1", - "grunt-bunyan": "^0.5.0" + "sinon": "~1.7.3", + "timekeeper": "0.0.4" } } From 82afad7afc1fe347e55c368c06a290794b979812 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 11 Jan 2019 12:11:36 +0000 Subject: [PATCH 433/709] Add **/*.map to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7fb78eef..048a75b9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ test/acceptance/js test/acceptance/fixtures/tmp compiles app.js +**/*.map .DS_Store *~ cache From e12ffdd53519100d72e4b7121d4426baed82f7a4 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 15 Jan 2019 11:12:21 +0000 Subject: [PATCH 434/709] Pass arguments to node, not to runuser --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index cb2580cb..f6d4bb1e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -20,4 +20,4 @@ chown -R node:node /app chown -R node:node /app/bin ./bin/install_texlive_gce.sh -exec runuser -u node "$@" \ No newline at end of file +exec runuser -u node "$*" From c269c308ef9d505eec72c816587af1e2c4ec59aa Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 15 Jan 2019 11:29:04 +0000 Subject: [PATCH 435/709] Correctly pass command with arguments to runuser --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index f6d4bb1e..ea295c7e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -20,4 +20,4 @@ chown -R node:node /app chown -R node:node /app/bin ./bin/install_texlive_gce.sh -exec runuser -u node "$*" +exec runuser -u node -- "$@" From 4c8b619ee882f646b21a7b91a2c8aad16e98906e Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Wed, 16 Jan 2019 15:11:49 +0000 Subject: [PATCH 436/709] Switch to node 10 --- Dockerfile | 4 +- app.coffee | 2 +- npm-shrinkwrap.json | 3043 +++++++++++------ package.json | 6 +- .../coffee/ExampleDocumentTests.coffee | 5 +- 5 files changed, 1991 insertions(+), 1069 deletions(-) diff --git a/Dockerfile b/Dockerfile index 861da2a0..d2bcb1ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:6.15.1 as app +FROM node:10.15.0 as app WORKDIR /app @@ -12,7 +12,7 @@ COPY . /app RUN npm run compile:all -FROM node:6.15.1 +FROM node:10.15.0 COPY --from=app /app /app diff --git a/app.coffee b/app.coffee index ec9fc803..fcf67c3c 100644 --- a/app.coffee +++ b/app.coffee @@ -146,7 +146,7 @@ app.get "/health_check", (req, res)-> app.get "/smoke_test_force", (req, res)-> smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res) -profiler = require "v8-profiler" +profiler = require "v8-profiler-node8" app.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") profiler.startProfiling("test") diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 83b6b503..9fe83ed3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,2652 +1,3571 @@ { "name": "node-clsi", "version": "0.1.4", + "lockfileVersion": 1, + "requires": true, "dependencies": { "@google-cloud/common": { "version": "0.27.0", - "from": "@google-cloud/common@>=0.27.0 <0.28.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.27.0.tgz" + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.27.0.tgz", + "integrity": "sha1-MdvVLXRy8mBt8FsZSIfbK1lb2tU=", + "requires": { + "@google-cloud/projectify": "^0.3.2", + "@google-cloud/promisify": "^0.3.0", + "@types/duplexify": "^3.5.0", + "@types/request": "^2.47.0", + "arrify": "^1.0.1", + "duplexify": "^3.6.0", + "ent": "^2.2.0", + "extend": "^3.0.1", + "google-auth-library": "^2.0.0", + "pify": "^4.0.0", + "retry-request": "^4.0.0" + } }, "@google-cloud/debug-agent": { "version": "3.0.1", - "from": "@google-cloud/debug-agent@>=3.0.0 <4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-3.0.1.tgz", + "integrity": "sha1-jGNJQkujDbEqTVaE7v19OvpmBQ4=", + "requires": { + "@google-cloud/common": "^0.27.0", + "@sindresorhus/is": "^0.13.0", + "acorn": "^5.0.3", + "coffeescript": "^2.0.0", + "console-log-level": "^1.4.0", + "extend": "^3.0.1", + "findit2": "^2.2.3", + "gcp-metadata": "^0.9.0", + "lodash.pickby": "^4.6.0", + "p-limit": "^2.0.0", + "pify": "^4.0.1", + "semver": "^5.5.0", + "source-map": "^0.6.1", + "split": "^1.0.0", + "teeny-request": "^3.11.1" + }, "dependencies": { "coffeescript": { "version": "2.3.2", - "from": "coffeescript@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.3.2.tgz" + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.3.2.tgz", + "integrity": "sha1-6FSnAg3+R7fPTdQSBC4y7x4mmBA=" } } }, "@google-cloud/profiler": { "version": "0.2.3", - "from": "@google-cloud/profiler@>=0.2.3 <0.3.0", "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-0.2.3.tgz", + "integrity": "sha1-Fj3738Mwuug1X+RuHlvgZTV7H1w=", + "requires": { + "@google-cloud/common": "^0.26.0", + "@types/console-log-level": "^1.4.0", + "@types/semver": "^5.5.0", + "bindings": "^1.2.1", + "console-log-level": "^1.4.0", + "delay": "^4.0.1", + "extend": "^3.0.1", + "gcp-metadata": "^0.9.0", + "nan": "^2.11.1", + "parse-duration": "^0.1.1", + "pify": "^4.0.0", + "pretty-ms": "^4.0.0", + "protobufjs": "~6.8.6", + "semver": "^5.5.0", + "teeny-request": "^3.3.0" + }, "dependencies": { "@google-cloud/common": { "version": "0.26.2", - "from": "@google-cloud/common@>=0.26.0 <0.27.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.26.2.tgz" + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.26.2.tgz", + "integrity": "sha1-nFTiRxqEqgMelaJIJJduCA8lVkU=", + "requires": { + "@google-cloud/projectify": "^0.3.2", + "@google-cloud/promisify": "^0.3.0", + "@types/duplexify": "^3.5.0", + "@types/request": "^2.47.0", + "arrify": "^1.0.1", + "duplexify": "^3.6.0", + "ent": "^2.2.0", + "extend": "^3.0.1", + "google-auth-library": "^2.0.0", + "pify": "^4.0.0", + "retry-request": "^4.0.0", + "through2": "^3.0.0" + } }, "through2": { "version": "3.0.0", - "from": "through2@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz" + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz", + "integrity": "sha1-RotGHfnNn8wXDyLr9oUuRn5Xj/I=", + "requires": { + "readable-stream": "2 || 3", + "xtend": "~4.0.1" + } } } }, "@google-cloud/projectify": { "version": "0.3.2", - "from": "@google-cloud/projectify@>=0.3.2 <0.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.2.tgz" + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.2.tgz", + "integrity": "sha1-7VTJjK5kbcA6dC6sKIGEoT0zpMI=" }, "@google-cloud/promisify": { "version": "0.3.1", - "from": "@google-cloud/promisify@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz" + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz", + "integrity": "sha1-9kHm2USo4KBe4MsQkd+mAIm+zbo=" }, "@google-cloud/trace-agent": { "version": "3.5.0", - "from": "@google-cloud/trace-agent@>=3.2.0 <4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-3.5.0.tgz", + "integrity": "sha1-GxU16eW26wAIXbbIWptLZJTjehY=", + "requires": { + "@google-cloud/common": "^0.28.0", + "builtin-modules": "^3.0.0", + "console-log-level": "^1.4.0", + "continuation-local-storage": "^3.2.1", + "extend": "^3.0.0", + "gcp-metadata": "^0.9.0", + "hex2dec": "^1.0.1", + "is": "^3.2.0", + "methods": "^1.1.1", + "require-in-the-middle": "^3.0.0", + "semver": "^5.4.1", + "shimmer": "^1.2.0", + "teeny-request": "^3.11.1", + "uuid": "^3.0.1" + }, "dependencies": { "@google-cloud/common": { "version": "0.28.0", - "from": "@google-cloud/common@>=0.28.0 <0.29.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.28.0.tgz" + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.28.0.tgz", + "integrity": "sha1-cOnTDHkOsPH/tuUpunI+ysO3prw=", + "requires": { + "@google-cloud/projectify": "^0.3.2", + "@google-cloud/promisify": "^0.3.0", + "@types/duplexify": "^3.5.0", + "@types/request": "^2.47.0", + "arrify": "^1.0.1", + "duplexify": "^3.6.0", + "ent": "^2.2.0", + "extend": "^3.0.1", + "google-auth-library": "^2.0.0", + "pify": "^4.0.0", + "retry-request": "^4.0.0" + } }, "uuid": { "version": "3.3.2", - "from": "uuid@>=3.0.1 <4.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" } } }, "@protobufjs/aspromise": { "version": "1.1.2", - "from": "@protobufjs/aspromise@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" }, "@protobufjs/base64": { "version": "1.1.2", - "from": "@protobufjs/base64@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha1-TIVzDlm5ofHzSQR9vyQpYDS7JzU=" }, "@protobufjs/codegen": { "version": "2.0.4", - "from": "@protobufjs/codegen@>=2.0.4 <3.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha1-fvN/DQEPsCitGtWXIuUG2SYoFcs=" }, "@protobufjs/eventemitter": { "version": "1.1.0", - "from": "@protobufjs/eventemitter@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" }, "@protobufjs/fetch": { "version": "1.1.0", - "from": "@protobufjs/fetch@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } }, "@protobufjs/float": { "version": "1.0.2", - "from": "@protobufjs/float@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" }, "@protobufjs/inquire": { "version": "1.1.0", - "from": "@protobufjs/inquire@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" }, "@protobufjs/path": { "version": "1.1.2", - "from": "@protobufjs/path@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" }, "@protobufjs/pool": { "version": "1.1.0", - "from": "@protobufjs/pool@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" }, "@protobufjs/utf8": { "version": "1.1.0", - "from": "@protobufjs/utf8@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@sindresorhus/is": { "version": "0.13.0", - "from": "@sindresorhus/is@>=0.13.0 <0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.13.0.tgz" + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.13.0.tgz", + "integrity": "sha1-qF0GwTZY0MyVhFN99KmUI0YfEUs=", + "requires": { + "symbol-observable": "^1.2.0" + } }, "@sinonjs/commons": { "version": "1.3.0", - "from": "@sinonjs/commons@>=1.2.0 <2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz", + "integrity": "sha1-UKJ1QBa28wqZTO2m2aCow2rdqEk=", + "requires": { + "type-detect": "4.0.8" + }, "dependencies": { "type-detect": { "version": "4.0.8", - "from": "type-detect@4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=" } } }, "@sinonjs/formatio": { "version": "3.1.0", - "from": "@sinonjs/formatio@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.1.0.tgz" + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.1.0.tgz", + "integrity": "sha1-asnR6xghmE2ExJlnJuRdFkbYzOU=", + "requires": { + "@sinonjs/samsam": "^2 || ^3" + } }, "@sinonjs/samsam": { "version": "3.0.2", - "from": "@sinonjs/samsam@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.0.2.tgz" + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.0.2.tgz", + "integrity": "sha1-ME+zO9VYWgst+KTIAfy0f6hNjkM=", + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash.get": "^4.4.2" + } }, "@types/caseless": { "version": "0.12.1", - "from": "@types/caseless@*", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz" + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", + "integrity": "sha1-l5TGnIOF0BkqzEcaVA0fjg0WIYo=" }, "@types/console-log-level": { "version": "1.4.0", - "from": "@types/console-log-level@>=1.4.0 <2.0.0", - "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz" + "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz", + "integrity": "sha1-7/ccQa689RyLpa2LBdfVQkviuPM=" }, "@types/duplexify": { "version": "3.6.0", - "from": "@types/duplexify@>=3.5.0 <4.0.0", - "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz" + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha1-38grZL06IWj1vSZESvFlvwI33Ng=", + "requires": { + "@types/node": "*" + } }, "@types/form-data": { "version": "2.2.1", - "from": "@types/form-data@*", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz" + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha1-7is7jqoRwJOCiZU2BrdFtzjFSx4=", + "requires": { + "@types/node": "*" + } }, "@types/geojson": { "version": "1.0.6", - "from": "@types/geojson@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz" + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", + "integrity": "sha1-PgKXJyjGkkjCrwjWCkjLuGgP/98=" }, "@types/long": { "version": "4.0.0", - "from": "@types/long@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz" + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", + "integrity": "sha1-cZVR0jUtMBrIuB23Mqy2vcKNve8=" }, "@types/node": { "version": "10.12.15", - "from": "@types/node@*", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz" + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz", + "integrity": "sha1-IOhWUbYv2GZW5Xycm8dxqxVwvFk=" }, "@types/request": { "version": "2.48.1", - "from": "@types/request@>=2.47.0 <3.0.0", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz" + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", + "integrity": "sha1-5ALWkapmcPu/8ZV7FfEnAjCrQvo=", + "requires": { + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" + } }, "@types/semver": { "version": "5.5.0", - "from": "@types/semver@>=5.5.0 <6.0.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz" + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha1-FGwqKe59O65L8vyydGNuJkyBPEU=" }, "@types/tough-cookie": { "version": "2.3.4", - "from": "@types/tough-cookie@*", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz" + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha1-ghh4uBv6uXG5OiZaVh1U6mH5BZ8=" + }, + "JSONStream": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", + "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } }, "abbrev": { "version": "1.1.1", - "from": "abbrev@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" }, "accepts": { "version": "1.3.5", - "from": "accepts@>=1.3.5 <1.4.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz" + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } }, "acorn": { "version": "5.7.3", - "from": "acorn@>=5.0.3 <6.0.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz" + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha1-Z6ojG/iBKXS4UjWpZ3Hra9B+onk=" }, "agent-base": { "version": "4.2.1", - "from": "agent-base@>=4.1.0 <5.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz" + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha1-2J5ZmfeXh1Z0wH2H8mD8Qeg+jKk=", + "requires": { + "es6-promisify": "^5.0.0" + } }, "ajv": { "version": "6.6.2", - "from": "ajv@>=6.5.5 <7.0.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz" + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", + "integrity": "sha1-ys7M9HS/P8POOxR0Q3EaJAY8ww0=", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } }, "ansi-regex": { "version": "2.1.1", - "from": "ansi-regex@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "1.1.0", - "from": "ansi-styles@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=" }, "aproba": { "version": "1.2.0", - "from": "aproba@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" }, "are-we-there-yet": { "version": "1.1.5", - "from": "are-we-there-yet@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz" + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } }, "argparse": { "version": "0.1.16", - "from": "argparse@>=0.1.11 <0.2.0", "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "requires": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + }, "dependencies": { "underscore": { "version": "1.7.0", - "from": "underscore@>=1.7.0 <1.8.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz" + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" }, "underscore.string": { "version": "2.4.0", - "from": "underscore.string@>=2.4.0 <2.5.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz" + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=" } } }, "array-flatten": { "version": "1.1.1", - "from": "array-flatten@1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-from": { "version": "2.1.1", - "from": "array-from@>=2.1.1 <3.0.0", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz" + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" }, "arrify": { "version": "1.0.1", - "from": "arrify@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" }, "asn1": { "version": "0.2.4", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz" + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", + "requires": { + "safer-buffer": "~2.1.0" + } }, "assert-plus": { "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assertion-error": { "version": "1.0.0", - "from": "assertion-error@1.0.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", "dev": true }, "async": { "version": "0.2.9", - "from": "async@0.2.9", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz" + "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", + "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" }, "async-listener": { "version": "0.6.10", - "from": "async-listener@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz" + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha1-p8l6vlcLpgLXgic8DeYKUePhfLw=", + "requires": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + } }, "asynckit": { "version": "0.4.0", - "from": "asynckit@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sign2": { "version": "0.7.0", - "from": "aws-sign2@>=0.7.0 <0.8.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.8.0", - "from": "aws4@>=1.8.0 <2.0.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz" + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" }, "axios": { "version": "0.18.0", - "from": "axios@>=0.18.0 <0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz" + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "requires": { + "follow-redirects": "^1.3.0", + "is-buffer": "^1.1.5" + } }, "balanced-match": { "version": "1.0.0", - "from": "balanced-match@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "bcrypt-pbkdf": { "version": "1.0.2", - "from": "bcrypt-pbkdf@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } }, "bignumber.js": { "version": "7.2.1", - "from": "bignumber.js@>=7.0.0 <8.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz" + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha1-gMBIdZ2CaACAfEv9Uh5Q7bulel8=" }, "bindings": { "version": "1.3.1", - "from": "bindings@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz" + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", + "integrity": "sha1-Ifx8bWfBhRbsWqooFbFF/3eybqU=" }, "bintrees": { "version": "1.0.1", - "from": "bintrees@1.0.1", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" }, "bl": { "version": "1.2.2", - "from": "bl@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz" - }, - "block-stream": { - "version": "0.0.9", - "from": "block-stream@*", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz" + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } }, "bluebird": { "version": "3.5.3", - "from": "bluebird@>=3.5.0 <4.0.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz" + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha1-fQHG+WFsmlGrD4xUmnnf5uwz76c=" }, "body-parser": { "version": "1.18.3", - "from": "body-parser@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } }, "brace-expansion": { "version": "1.1.11", - "from": "brace-expansion@>=1.1.7 <2.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, "browser-stdout": { "version": "1.3.0", - "from": "browser-stdout@1.3.0", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, "buffer-alloc": { "version": "1.2.0", - "from": "buffer-alloc@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } }, "buffer-alloc-unsafe": { "version": "1.1.0", - "from": "buffer-alloc-unsafe@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA=" }, "buffer-equal-constant-time": { "version": "1.0.1", - "from": "buffer-equal-constant-time@1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, "buffer-fill": { "version": "1.0.0", - "from": "buffer-fill@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" }, "buffer-from": { "version": "1.1.1", - "from": "buffer-from@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=" }, "builtin-modules": { "version": "3.0.0", - "from": "builtin-modules@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.0.0.tgz" + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.0.0.tgz", + "integrity": "sha1-Hlh9RLAGYg2QKGzHqSOLvGEpyrE=" }, "bunyan": { "version": "0.22.3", - "from": "bunyan@>=0.22.1 <0.23.0", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", - "dev": true + "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", + "dev": true, + "requires": { + "dtrace-provider": "0.2.8", + "mv": "~2" + } }, "buster-core": { "version": "0.6.4", - "from": "buster-core@0.6.4", "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", + "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", "dev": true }, "buster-format": { "version": "0.5.6", - "from": "buster-format@>=0.5.0 <0.6.0", "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", - "dev": true + "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", + "dev": true, + "requires": { + "buster-core": "=0.6.4" + } }, "bytes": { "version": "3.0.0", - "from": "bytes@3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, "caseless": { "version": "0.12.0", - "from": "caseless@>=0.12.0 <0.13.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chai": { "version": "1.8.1", - "from": "chai@>=1.8.1 <1.9.0", "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", - "dev": true + "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", + "dev": true, + "requires": { + "assertion-error": "1.0.0", + "deep-eql": "0.1.3" + } }, "chalk": { "version": "0.5.1", - "from": "chalk@>=0.5.0 <0.6.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + }, "dependencies": { "ansi-regex": { "version": "0.2.1", - "from": "ansi-regex@^0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=" }, "strip-ansi": { "version": "0.3.0", - "from": "strip-ansi@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz" + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "requires": { + "ansi-regex": "^0.2.1" + } }, "supports-color": { "version": "0.2.0", - "from": "supports-color@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz" + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=" } } }, "check-error": { "version": "1.0.2", - "from": "check-error@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" }, "chownr": { "version": "1.1.1", - "from": "chownr@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha1-VHJri4//TfBTxCGH6AH7RBLfFJQ=" }, "cls-bluebird": { "version": "2.1.0", - "from": "cls-bluebird@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz" - }, - "co": { - "version": "4.6.0", - "from": "co@>=4.6.0 <5.0.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", + "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", + "requires": { + "is-bluebird": "^1.0.2", + "shimmer": "^1.1.0" + } }, "code-point-at": { "version": "1.1.0", - "from": "code-point-at@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "coffee-script": { "version": "1.6.0", - "from": "coffee-script@1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" + }, + "coffeescript": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.6.0.tgz", + "integrity": "sha1-bdTeHrYveE2MjYCWdVLLpUf/2d4=", + "dev": true }, "colors": { "version": "0.6.2", - "from": "colors@>=0.6.2 <0.7.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz" + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" }, "combined-stream": { "version": "1.0.7", - "from": "combined-stream@>=1.0.6 <1.1.0", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz" + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha1-LR0kMXr7ir6V1tLAsHtXgTU52Cg=", + "requires": { + "delayed-stream": "~1.0.0" + } }, "commander": { "version": "2.0.0", - "from": "commander@2.0.0", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz" + "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", - "from": "concat-stream@>=1.6.2 <1.7.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz" + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } }, "console-control-strings": { "version": "1.1.0", - "from": "console-control-strings@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "console-log-level": { "version": "1.4.0", - "from": "console-log-level@>=1.4.0 <2.0.0", - "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.0.tgz" + "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.0.tgz", + "integrity": "sha1-QDWBi+6jflhQoMA8jUUMpfLNEhc=" }, "content-disposition": { "version": "0.5.2", - "from": "content-disposition@0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz" + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" }, "content-type": { "version": "1.0.4", - "from": "content-type@>=1.0.4 <1.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" }, "continuation-local-storage": { "version": "3.2.1", - "from": "continuation-local-storage@>=3.2.1 <4.0.0", - "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz" + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha1-EfYT906RT+mzTJKtLSj+auHbf/s=", + "requires": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } }, "cookie": { "version": "0.3.1", - "from": "cookie@0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz" + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, "cookie-signature": { "version": "1.0.6", - "from": "cookie-signature@1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "core-util-is": { "version": "1.0.2", - "from": "core-util-is@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "dashdash": { "version": "1.14.1", - "from": "dashdash@>=1.12.0 <2.0.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } }, "dateformat": { "version": "1.0.2-1.2.3", - "from": "dateformat@1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz" + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=" }, "debug": { "version": "2.6.9", - "from": "debug@2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } }, "deep-eql": { "version": "0.1.3", - "from": "deep-eql@0.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "dev": true + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + } }, "deep-extend": { "version": "0.6.0", - "from": "deep-extend@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha1-xPp8lUBKF6nD6Mp+FTcxK3NjMKw=" }, "delay": { "version": "4.1.0", - "from": "delay@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/delay/-/delay-4.1.0.tgz" + "resolved": "https://registry.npmjs.org/delay/-/delay-4.1.0.tgz", + "integrity": "sha1-R0zSiAnaQdGgSKcKHYNfR6w3fNI=" }, "delayed-stream": { "version": "1.0.0", - "from": "delayed-stream@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", - "from": "delegates@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", - "from": "depd@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "destroy": { "version": "1.0.4", - "from": "destroy@>=1.0.4 <1.1.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "detect-libc": { "version": "1.0.3", - "from": "detect-libc@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz" + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "diff": { "version": "1.0.7", - "from": "diff@1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz" + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" }, "docker-modem": { "version": "1.0.7", - "from": "docker-modem@>=1.0.0 <1.1.0", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.7.tgz", + "integrity": "sha1-aXAqlcBg7rZ3X3nM3Mc05ZRpcqQ=", + "requires": { + "JSONStream": "1.3.2", + "debug": "^3.2.5", + "readable-stream": "~1.0.26-4", + "split-ca": "^1.0.0" + }, "dependencies": { "debug": { "version": "3.2.6", - "from": "debug@>=3.2.5 <4.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "requires": { + "ms": "^2.1.1" + } }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "ms": { "version": "2.1.1", - "from": "ms@>=2.1.1 <3.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" }, "readable-stream": { "version": "1.0.34", - "from": "readable-stream@>=1.0.26-4 <1.1.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, "dockerode": { "version": "2.5.7", - "from": "dockerode@>=2.5.3 <3.0.0", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.7.tgz" + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.7.tgz", + "integrity": "sha1-E9yewPfzU6wOUSJJ538y0aqhGZ4=", + "requires": { + "concat-stream": "~1.6.2", + "docker-modem": "1.0.x", + "tar-fs": "~1.16.3" + } }, "dottie": { "version": "2.0.1", - "from": "dottie@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz" + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz", + "integrity": "sha1-aXrZ1yAE23V00h+JJGajwoWJNlk=" }, "dtrace-provider": { "version": "0.2.8", - "from": "dtrace-provider@0.2.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", + "integrity": "sha1-4kPxkhmqlfvw2PL/sH9b1k6U/iA=", "dev": true, "optional": true }, "duplexify": { "version": "3.6.1", - "from": "duplexify@>=3.6.0 <4.0.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz" + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", + "integrity": "sha1-saeinEq/1jlYXvrszoDWZrHjQSU=", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } }, "ecc-jsbn": { "version": "0.1.2", - "from": "ecc-jsbn@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } }, "ecdsa-sig-formatter": { "version": "1.0.10", - "from": "ecdsa-sig-formatter@1.0.10", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz" + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", + "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "requires": { + "safe-buffer": "^5.0.1" + } }, "ee-first": { "version": "1.1.1", - "from": "ee-first@1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "emitter-listener": { "version": "1.1.2", - "from": "emitter-listener@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz" + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha1-VrFA6PaZI3Wz18ssqxzHQy2WMug=", + "requires": { + "shimmer": "^1.2.0" + } }, "encodeurl": { "version": "1.0.2", - "from": "encodeurl@>=1.0.2 <1.1.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "end-of-stream": { "version": "1.4.1", - "from": "end-of-stream@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz" + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", + "requires": { + "once": "^1.4.0" + } }, "ent": { "version": "2.2.0", - "from": "ent@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz" + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" }, "es6-promise": { "version": "4.2.5", - "from": "es6-promise@>=4.0.3 <5.0.0", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz" + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", + "integrity": "sha1-2m0NVpLvtGHggsFIF/4kJ9j10FQ=" }, "es6-promisify": { "version": "5.0.0", - "from": "es6-promisify@>=5.0.0 <6.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz" + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } }, "escape-html": { "version": "1.0.3", - "from": "escape-html@>=1.0.3 <1.1.0", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", - "from": "escape-string-regexp@1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "esprima": { "version": "1.0.4", - "from": "esprima@>=1.0.2 <1.1.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" }, "etag": { "version": "1.8.1", - "from": "etag@>=1.8.1 <1.9.0", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "eventemitter2": { "version": "0.4.14", - "from": "eventemitter2@>=0.4.13 <0.5.0", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=" }, "exit": { "version": "0.1.2", - "from": "exit@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" }, "express": { "version": "4.16.4", - "from": "express@>=4.2.0 <5.0.0", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha1-/d72GSYQniTFFeqX/S8b2/Yt8S4=", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, "dependencies": { "statuses": { "version": "1.4.0", - "from": "statuses@>=1.4.0 <1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" } } }, "extend": { "version": "3.0.2", - "from": "extend@>=3.0.1 <4.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" }, "extsprintf": { "version": "1.3.0", - "from": "extsprintf@1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "2.0.1", - "from": "fast-deep-equal@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz" + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", - "from": "fast-json-stable-stringify@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "finalhandler": { "version": "1.1.1", - "from": "finalhandler@1.1.1", "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha1-7r9O2EAHnIP0JJA4ydcDAIMBsQU=", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, "dependencies": { "statuses": { "version": "1.4.0", - "from": "statuses@>=1.4.0 <1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" } } }, "findit2": { "version": "2.2.3", - "from": "findit2@>=2.2.3 <3.0.0", - "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz" + "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz", + "integrity": "sha1-WKRmaX34piBc39vzlVNri9d3pfY=" }, "findup-sync": { "version": "0.1.3", - "from": "findup-sync@>=0.1.2 <0.2.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "requires": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, "dependencies": { "glob": { "version": "3.2.11", - "from": "glob@>=3.2.9 <3.3.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz" + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "requires": { + "inherits": "2", + "minimatch": "0.3" + } }, "lodash": { "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" }, "lru-cache": { "version": "2.7.3", - "from": "lru-cache@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" }, "minimatch": { "version": "0.3.0", - "from": "minimatch@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz" + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } } } }, "follow-redirects": { "version": "1.5.10", - "from": "follow-redirects@>=1.3.0 <2.0.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, "dependencies": { "debug": { "version": "3.1.0", - "from": "debug@3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } } } }, "forever-agent": { "version": "0.6.1", - "from": "forever-agent@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.3", - "from": "form-data@>=2.3.2 <2.4.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } }, "forwarded": { "version": "0.1.2", - "from": "forwarded@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz" + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, "fresh": { "version": "0.5.2", - "from": "fresh@0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fs-constants": { "version": "1.0.0", - "from": "fs-constants@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=" }, "fs-extra": { "version": "0.16.5", - "from": "fs-extra@>=0.16.3 <0.17.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz" + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", + "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", + "requires": { + "graceful-fs": "^3.0.5", + "jsonfile": "^2.0.0", + "rimraf": "^2.2.8" + } }, "fs-minipass": { "version": "1.2.5", - "from": "fs-minipass@>=1.2.5 <2.0.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz" + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha1-BsJ3IYRU7CiN93raVKA7hwKqy50=", + "requires": { + "minipass": "^2.2.1" + } }, "fs.realpath": { "version": "1.0.0", - "from": "fs.realpath@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - }, - "fstream": { - "version": "1.0.11", - "from": "fstream@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "dependencies": { - "graceful-fs": { - "version": "4.1.15", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz" - }, - "mkdirp": { - "version": "0.5.1", - "from": "mkdirp@>=0.5.0 >=0.0.0 <1.0.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - } - } - }, - "fstream-ignore": { - "version": "1.0.5", - "from": "fstream-ignore@>=1.0.5 <2.0.0", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz" + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "gauge": { "version": "2.7.4", - "from": "gauge@>=2.7.3 <2.8.0", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz" + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } }, "gaxios": { "version": "1.0.4", - "from": "gaxios@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.0.4.tgz" + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.0.4.tgz", + "integrity": "sha512-5IqL40mfNrpgUQpzWkVZHLjDq62QHVn5+HmwI0Hf1haKPzAE6DftUxoGAf9pnEARwlK1A6tWmtjxLVl/kCzCFA==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0" + } }, "gcp-metadata": { "version": "0.9.3", - "from": "gcp-metadata@>=0.9.0 <0.10.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz" + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz", + "integrity": "sha1-H510lfdGChRSZIHynhFZbdVj3SY=", + "requires": { + "gaxios": "^1.0.2", + "json-bigint": "^0.3.0" + } }, "generic-pool": { "version": "3.4.2", - "from": "generic-pool@>=3.4.0 <4.0.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz" + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz", + "integrity": "sha1-kv9xllINZwg5pnMICSoSqt8valk=" }, "get-func-name": { "version": "2.0.0", - "from": "get-func-name@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" }, "getobject": { "version": "0.1.0", - "from": "getobject@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz" + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=" }, "getpass": { "version": "0.1.7", - "from": "getpass@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } }, "glob": { "version": "7.1.3", - "from": "glob@>=7.0.5 <8.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz" + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha1-OWCDLT8VdBCDQtr9OmezMsCWnfE=", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } }, "google-auth-library": { "version": "2.0.2", - "from": "google-auth-library@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-2.0.2.tgz", + "integrity": "sha1-ejFdIDZ0Svavyth7IQ7mY4tA9Xs=", + "requires": { + "axios": "^0.18.0", + "gcp-metadata": "^0.7.0", + "gtoken": "^2.3.0", + "https-proxy-agent": "^2.2.1", + "jws": "^3.1.5", + "lru-cache": "^5.0.0", + "semver": "^5.5.0" + }, "dependencies": { "gcp-metadata": { "version": "0.7.0", - "from": "gcp-metadata@>=0.7.0 <0.8.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz" + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz", + "integrity": "sha1-bDXbtSvaMqQnu5yY9UI33dG1QG8=", + "requires": { + "axios": "^0.18.0", + "extend": "^3.0.1", + "retry-axios": "0.3.2" + } } } }, "google-p12-pem": { "version": "1.0.3", - "from": "google-p12-pem@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.3.tgz" + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.3.tgz", + "integrity": "sha1-PYrMFAVzM5pbynsvaksga76m2Nc=", + "requires": { + "node-forge": "^0.7.5", + "pify": "^4.0.0" + } }, "graceful-fs": { "version": "3.0.11", - "from": "graceful-fs@>=3.0.5 <4.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "requires": { + "natives": "^1.1.0" + } }, "growl": { "version": "1.7.0", - "from": "growl@1.7.x", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz" + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" }, "grunt": { "version": "0.4.5", - "from": "grunt@>=0.4.5 <0.5.0", "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "requires": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, "dependencies": { "async": { "version": "0.1.22", - "from": "async@>=0.1.22 <0.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=" }, "coffee-script": { "version": "1.3.3", - "from": "coffee-script@>=1.3.3 <1.4.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz" + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=" }, "glob": { "version": "3.1.21", - "from": "glob@>=3.1.21 <3.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz" + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + } }, "graceful-fs": { "version": "1.2.3", - "from": "graceful-fs@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=" }, "iconv-lite": { "version": "0.2.11", - "from": "iconv-lite@>=0.2.11 <0.3.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=" }, "inherits": { "version": "1.0.2", - "from": "inherits@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=" }, "lodash": { "version": "0.9.2", - "from": "lodash@>=0.9.2 <0.10.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=" }, "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" }, "minimatch": { "version": "0.2.14", - "from": "minimatch@>=0.2.12 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } }, "nopt": { "version": "1.0.10", - "from": "nopt@>=1.0.10 <1.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } }, "rimraf": { "version": "2.2.8", - "from": "rimraf@>=2.2.8 <2.3.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" } } }, "grunt-bunyan": { "version": "0.5.0", - "from": "grunt-bunyan@>=0.5.0 <0.6.0", "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", + "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", + "requires": { + "lodash": "~2.4.1" + }, "dependencies": { "lodash": { "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" } } }, "grunt-contrib-clean": { "version": "0.6.0", - "from": "grunt-contrib-clean@>=0.6.0 <0.7.0", "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.6.0.tgz", + "integrity": "sha1-9TLbpLghJnTHwBPhRr2mY4uQSPY=", + "requires": { + "rimraf": "~2.2.1" + }, "dependencies": { "rimraf": { "version": "2.2.8", - "from": "rimraf@~2.2.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" } } }, "grunt-contrib-coffee": { "version": "0.11.1", - "from": "grunt-contrib-coffee@>=0.11.0 <0.12.0", "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", + "integrity": "sha1-+v48nuikQryNF9WlwZ/I5i2fP0U=", + "requires": { + "chalk": "~0.5.0", + "coffee-script": "~1.7.0", + "lodash": "~2.4.1" + }, "dependencies": { "coffee-script": { "version": "1.7.1", - "from": "coffee-script@>=1.7.0 <1.8.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz" + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", + "integrity": "sha1-YplqhheAx15tUGnROCJyO3NAS/w=", + "requires": { + "mkdirp": "~0.3.5" + } }, "lodash": { "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" } } }, "grunt-execute": { "version": "0.2.2", - "from": "grunt-execute@>=0.2.2 <0.3.0", - "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz" + "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz", + "integrity": "sha1-TpRf5XlZzA3neZCDtrQq7ZYWNQo=" }, "grunt-legacy-log": { "version": "0.1.3", - "from": "grunt-legacy-log@>=0.1.0 <0.2.0", "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "requires": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, "dependencies": { "lodash": { "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" }, "underscore.string": { "version": "2.3.3", - "from": "underscore.string@>=2.3.3 <2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=" } } }, "grunt-legacy-log-utils": { "version": "0.1.1", - "from": "grunt-legacy-log-utils@>=0.1.1 <0.2.0", "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "requires": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, "dependencies": { "lodash": { "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" }, "underscore.string": { "version": "2.3.3", - "from": "underscore.string@>=2.3.3 <2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=" } } }, "grunt-legacy-util": { "version": "0.2.0", - "from": "grunt-legacy-util@>=0.2.0 <0.3.0", "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "requires": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, "dependencies": { "async": { "version": "0.1.22", - "from": "async@~0.1.22", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=" }, "lodash": { "version": "0.9.2", - "from": "lodash@~0.9.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=" } } }, "grunt-mocha-test": { "version": "0.11.0", - "from": "grunt-mocha-test@>=0.11.0 <0.12.0", "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.11.0.tgz", + "integrity": "sha1-deQboQdZDkrL0phgklwBLkQ8oyI=", + "requires": { + "fs-extra": "~0.9.1", + "hooker": "~0.2.3", + "mocha": "~1.20.0" + }, "dependencies": { "fs-extra": { "version": "0.9.1", - "from": "fs-extra@>=0.9.1 <0.10.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.9.1.tgz" + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.9.1.tgz", + "integrity": "sha1-h9v8ATg6jdzn2dVJbzYIVkiJ8VY=", + "requires": { + "jsonfile": "~1.1.0", + "mkdirp": "^0.5.0", + "ncp": "^0.5.1", + "rimraf": "^2.2.8" + } }, "glob": { "version": "3.2.3", - "from": "glob@3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz" + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "requires": { + "graceful-fs": "~2.0.0", + "inherits": "2", + "minimatch": "~0.2.11" + } }, "graceful-fs": { "version": "2.0.3", - "from": "graceful-fs@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" }, "jsonfile": { "version": "1.1.1", - "from": "jsonfile@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.1.1.tgz", + "integrity": "sha1-2k/WrXfxolUgPqY8e8Mtwx72RDM=" }, "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" }, "minimatch": { "version": "0.2.14", - "from": "minimatch@~0.2.11", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } }, "mkdirp": { "version": "0.5.1", - "from": "mkdirp@>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } }, "mocha": { "version": "1.20.1", - "from": "mocha@>=1.20.0 <1.21.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.20.1.tgz", + "integrity": "sha1-80ODLZ/gx9l8ZPxwRI9RNt+f7Vs=", + "requires": { + "commander": "2.0.0", + "debug": "*", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.x", + "jade": "0.26.3", + "mkdirp": "0.3.5" + }, "dependencies": { "mkdirp": { "version": "0.3.5", - "from": "mkdirp@0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" } } }, "ncp": { "version": "0.5.1", - "from": "ncp@>=0.5.1 <0.6.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz" + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", + "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=" } } }, "gtoken": { "version": "2.3.0", - "from": "gtoken@>=2.3.0 <3.0.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.0.tgz", + "integrity": "sha512-Jc9/8mV630cZE9FC5tIlJCZNdUjwunvlwOtCz6IDlaiB4Sz68ki29a1+q97sWTnTYroiuF9B135rod9zrQdHLw==", + "requires": { + "axios": "^0.18.0", + "google-p12-pem": "^1.0.0", + "jws": "^3.1.4", + "mime": "^2.2.0", + "pify": "^3.0.0" + }, "dependencies": { "mime": { "version": "2.4.0", - "from": "mime@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz" + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", + "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==" }, "pify": { "version": "3.0.0", - "from": "pify@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" } } }, "har-schema": { "version": "2.0.0", - "from": "har-schema@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.3", - "from": "har-validator@>=5.1.0 <5.2.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz" + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } }, "has-ansi": { "version": "0.1.0", - "from": "has-ansi@>=0.1.0 <0.2.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "requires": { + "ansi-regex": "^0.2.0" + }, "dependencies": { "ansi-regex": { "version": "0.2.1", - "from": "ansi-regex@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=" } } }, "has-flag": { "version": "2.0.0", - "from": "has-flag@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, "has-unicode": { "version": "2.0.1", - "from": "has-unicode@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "he": { "version": "1.1.1", - "from": "he@1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, "heapdump": { "version": "0.3.12", - "from": "heapdump@>=0.3.5 <0.4.0", - "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.12.tgz" + "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.12.tgz", + "integrity": "sha1-ViO+eBaoqSqy1CsbQi+egppY2ok=", + "requires": { + "nan": "^2.11.1" + } }, "hex2dec": { "version": "1.1.1", - "from": "hex2dec@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.1.tgz" - }, - "hoek": { - "version": "2.16.3", - "from": "hoek@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.1.tgz", + "integrity": "sha512-E1nK1KCHaX9NvY3wkCMpZxj3oGokO5fgjcKUBaOgSkkvNogm8ngb8isKtzlxnLT37/JXLODVYXz9ti99Bxz8gg==" }, "hooker": { "version": "0.2.3", - "from": "hooker@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=" }, "http-errors": { "version": "1.6.3", - "from": "http-errors@>=1.6.3 <1.7.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } }, "http-signature": { "version": "1.2.0", - "from": "http-signature@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } }, "https-proxy-agent": { "version": "2.2.1", - "from": "https-proxy-agent@>=2.2.1 <3.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha1-UVUpcPoE1yPgTFbQQXjD+SWSu8A=", + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + }, "dependencies": { "debug": { "version": "3.2.6", - "from": "debug@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "requires": { + "ms": "^2.1.1" + } }, "ms": { "version": "2.1.1", - "from": "ms@>=2.1.1 <3.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" } } }, "iconv-lite": { "version": "0.4.23", - "from": "iconv-lite@0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz" + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha1-KXhx9jvlB63Pv8pxXQzQ7thOmmM=", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "ignore-walk": { "version": "3.0.1", - "from": "ignore-walk@>=3.0.1 <4.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz" + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha1-qD5i59JyrA47VRqqgoMaGbafgvg=", + "requires": { + "minimatch": "^3.0.4" + } }, "inflection": { "version": "1.12.0", - "from": "inflection@1.12.0", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz" + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" }, "inflight": { "version": "1.0.6", - "from": "inflight@>=1.0.4 <2.0.0", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } }, "inherits": { "version": "2.0.3", - "from": "inherits@2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", - "from": "ini@>=1.3.0 <1.4.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz" + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" }, "ipaddr.js": { "version": "1.8.0", - "from": "ipaddr.js@1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz" + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" }, "is": { "version": "3.3.0", - "from": "is@>=3.2.0 <4.0.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz" + "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", + "integrity": "sha1-Yc/23TxBk9uUo9YlggcrROVkXXk=" }, "is-bluebird": { "version": "1.0.2", - "from": "is-bluebird@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" }, "is-buffer": { "version": "1.1.6", - "from": "is-buffer@>=1.1.5 <2.0.0", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" }, "is-fullwidth-code-point": { "version": "1.0.0", - "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } }, "is-typedarray": { "version": "1.0.0", - "from": "is-typedarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "isarray": { "version": "1.0.0", - "from": "isarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isstream": { "version": "0.1.2", - "from": "isstream@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "jade": { "version": "0.26.3", - "from": "jade@0.26.3", "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, "dependencies": { "commander": { "version": "0.6.1", - "from": "commander@0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" }, "mkdirp": { "version": "0.3.0", - "from": "mkdirp@0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" } } }, "js-yaml": { "version": "2.0.5", - "from": "js-yaml@>=2.0.5 <2.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz" + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "requires": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + } }, "jsbn": { "version": "0.1.1", - "from": "jsbn@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "json-bigint": { "version": "0.3.0", - "from": "json-bigint@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz" + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", + "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "requires": { + "bignumber.js": "^7.0.0" + } }, "json-schema": { "version": "0.2.3", - "from": "json-schema@0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz" + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", - "from": "json-schema-traverse@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - }, - "json-stable-stringify": { - "version": "1.0.1", - "from": "json-stable-stringify@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" }, "json-stringify-safe": { "version": "5.0.1", - "from": "json-stringify-safe@5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonfile": { "version": "2.4.0", - "from": "jsonfile@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "^4.1.6" + }, "dependencies": { "graceful-fs": { "version": "4.1.15", - "from": "graceful-fs@>=4.1.6 <5.0.0", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha1-/7cD4QZuig7qpMi4C6klPu77+wA=", "optional": true } } }, - "jsonify": { - "version": "0.0.0", - "from": "jsonify@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" - }, "jsonparse": { "version": "1.3.1", - "from": "jsonparse@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" - }, - "JSONStream": { - "version": "1.3.2", - "from": "JSONStream@1.3.2", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz" + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" }, "jsprim": { "version": "1.4.1", - "from": "jsprim@>=1.2.2 <2.0.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz" + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } }, "just-extend": { "version": "4.0.2", - "from": "just-extend@>=4.0.2 <5.0.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz" + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=" }, "jwa": { "version": "1.1.6", - "from": "jwa@>=1.1.5 <2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz" + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", + "integrity": "sha1-hyQOdsmAjb3hh4PPImTvSSnuUOY=", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.10", + "safe-buffer": "^5.0.1" + } }, "jws": { "version": "3.1.5", - "from": "jws@>=3.1.5 <4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz" + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", + "integrity": "sha1-gNEtBbKT0ehB58uLTmnlYa3Pg08=", + "requires": { + "jwa": "^1.1.5", + "safe-buffer": "^5.0.1" + } }, "lockfile": { "version": "1.0.4", - "from": "lockfile@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz" + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha1-B/gZ0lrkj4flOOZXi2lkpJgaVgk=", + "requires": { + "signal-exit": "^3.0.2" + } }, "lodash": { "version": "4.17.11", - "from": "lodash@>=4.17.1 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" }, "lodash.get": { "version": "4.4.2", - "from": "lodash.get@>=4.4.2 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, "lodash.pickby": { "version": "4.6.0", - "from": "lodash.pickby@>=4.6.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz" + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=" }, "logger-sharelatex": { - "version": "1.5.9", + "version": "git+https://github.com/sharelatex/logger-sharelatex.git#e8e1b95052f62e107336053e4a983f81cdbdf589", "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.9", - "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#e8e1b95052f62e107336053e4a983f81cdbdf589", + "requires": { + "bunyan": "1.5.1", + "chai": "^4.2.0", + "coffee-script": "1.12.4", + "grunt": "^0.4.5", + "grunt-bunyan": "^0.5.0", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-coffee": "^0.11.0", + "grunt-execute": "^0.2.2", + "grunt-mocha-test": "^0.11.0", + "raven": "^1.1.3", + "sandboxed-module": "^2.0.3", + "sinon": "^7.2.2", + "timekeeper": "^1.0.0" + }, "dependencies": { "assertion-error": { "version": "1.1.0", - "from": "assertion-error@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs=" }, "bunyan": { "version": "1.5.1", - "from": "bunyan@1.5.1", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz" + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", + "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", + "requires": { + "dtrace-provider": "~0.6", + "mv": "~2", + "safe-json-stringify": "~1" + } }, "chai": { "version": "4.2.0", - "from": "chai@latest", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz" + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha1-dgqnLPION5XoSxKHfODoNzeqKeU=", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } }, "coffee-script": { "version": "1.12.4", - "from": "coffee-script@1.12.4", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.4.tgz" + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.4.tgz", + "integrity": "sha1-/hvO2X/h+zknuZjytFYW4GWL4f8=" }, "deep-eql": { "version": "3.0.1", - "from": "deep-eql@>=3.0.1 <4.0.0", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha1-38lARACtHI/gI+faHfHBR8S0RN8=", + "requires": { + "type-detect": "^4.0.0" + } }, "diff": { "version": "3.5.0", - "from": "diff@>=3.5.0 <4.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=" }, "dtrace-provider": { "version": "0.6.0", - "from": "dtrace-provider@>=0.6.0 <0.7.0", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", - "optional": true + "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", + "optional": true, + "requires": { + "nan": "^2.0.8" + } }, "has-flag": { "version": "3.0.0", - "from": "has-flag@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "sandboxed-module": { "version": "2.0.3", - "from": "sandboxed-module@latest", - "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-2.0.3.tgz" + "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-2.0.3.tgz", + "integrity": "sha1-x+VFkzm7y6KMUwPusz9ug4e/upY=", + "requires": { + "require-like": "0.1.2", + "stack-trace": "0.0.9" + } }, "sinon": { "version": "7.2.2", - "from": "sinon@latest", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz" + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz", + "integrity": "sha1-OI7KvUL6k8WSv8cdNacIlNWgygc=", + "requires": { + "@sinonjs/commons": "^1.2.0", + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/samsam": "^3.0.2", + "diff": "^3.5.0", + "lolex": "^3.0.0", + "nise": "^1.4.7", + "supports-color": "^5.5.0" + } }, "supports-color": { "version": "5.5.0", - "from": "supports-color@>=5.5.0 <6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", + "requires": { + "has-flag": "^3.0.0" + } }, "timekeeper": { "version": "1.0.0", - "from": "timekeeper@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-1.0.0.tgz", + "integrity": "sha1-Lziu4elLEd1m2FgP8aqdzGoroNg=" }, "type-detect": { "version": "4.0.8", - "from": "type-detect@>=4.0.5 <5.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=" } } }, "lolex": { "version": "3.0.0", - "from": "lolex@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz" + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz", + "integrity": "sha1-8E7hqKoT9g8avXsOj0IT7HLsGT4=" }, "long": { "version": "4.0.0", - "from": "long@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz" + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha1-mntxz7fTYaGU6lVSQckvdGjVvyg=" }, "lru-cache": { "version": "5.1.1", - "from": "lru-cache@>=5.0.0 <6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha1-HaJ+ZxAnGUdpXa9oSOhH8B2EuSA=", + "requires": { + "yallist": "^3.0.2" + } }, "lsmod": { "version": "1.0.0", - "from": "lsmod@1.0.0", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", + "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" }, "lynx": { "version": "0.0.11", - "from": "lynx@0.0.11", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz" + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", + "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", + "requires": { + "mersenne": "~0.0.3", + "statsd-parser": "~0.0.4" + } }, "media-typer": { "version": "0.3.0", - "from": "media-typer@0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "merge-descriptors": { "version": "1.0.1", - "from": "merge-descriptors@1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "mersenne": { "version": "0.0.4", - "from": "mersenne@>=0.0.3 <0.1.0", - "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz" + "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", + "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" }, "methods": { "version": "1.1.2", - "from": "methods@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "metrics-sharelatex": { - "version": "2.0.12", + "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#3ac1621ef049e2f2d88a83b3a41011333d609662", "from": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.12", - "resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#3ac1621ef049e2f2d88a83b3a41011333d609662", + "requires": { + "@google-cloud/debug-agent": "^3.0.0", + "@google-cloud/profiler": "^0.2.3", + "@google-cloud/trace-agent": "^3.2.0", + "coffee-script": "1.6.0", + "lynx": "~0.1.1", + "prom-client": "^11.1.3", + "underscore": "~1.6.0" + }, "dependencies": { "coffee-script": { "version": "1.6.0", - "from": "coffee-script@1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", + "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" }, "lynx": { "version": "0.1.1", - "from": "lynx@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz" + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", + "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", + "requires": { + "mersenne": "~0.0.3", + "statsd-parser": "~0.0.4" + } }, "underscore": { "version": "1.6.0", - "from": "underscore@>=1.6.0 <1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" } } }, "mime": { "version": "1.4.1", - "from": "mime@1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz" + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=" }, "mime-db": { "version": "1.37.0", - "from": "mime-db@>=1.37.0 <1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz" + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha1-C2oM5v2+lXbiXx8tL96IMNwK0Ng=" }, "mime-types": { "version": "2.1.21", - "from": "mime-types@>=2.1.18 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz" + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha1-KJlaoey3cHQv5q5+WPkYHHRLP5Y=", + "requires": { + "mime-db": "~1.37.0" + } }, "minimatch": { "version": "3.0.4", - "from": "minimatch@>=3.0.4 <4.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "^1.1.7" + } }, "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { "version": "2.3.5", - "from": "minipass@>=2.3.4 <3.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz" + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha1-ys6+SSAiSX9law8PUeJoKp7S2Eg=", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } }, "minizlib": { "version": "1.2.1", - "from": "minizlib@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz" + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha1-3SfqYTYkPHyIBoToZyuzpF/ZthQ=", + "requires": { + "minipass": "^2.2.1" + } }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" }, "mocha": { "version": "4.1.0", - "from": "mocha@>=4.0.1 <5.0.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "integrity": "sha1-fYbPvPNcuCnidUwy4XNV7AUzh5Q=", "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, "dependencies": { "commander": { "version": "2.11.0", - "from": "commander@2.11.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=", "dev": true }, "debug": { "version": "3.1.0", - "from": "debug@3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "dev": true + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, "diff": { "version": "3.3.1", - "from": "diff@3.3.1", "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU=", "dev": true }, "glob": { "version": "7.1.2", - "from": "glob@7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "dev": true + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } }, "growl": { "version": "1.10.3", - "from": "growl@1.10.3", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha1-GSa6kM8+3+KttJJ/WIC8IsZseQ8=", "dev": true }, "mkdirp": { "version": "0.5.1", - "from": "mkdirp@0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "dev": true + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } } } }, "module-details-from-path": { "version": "1.0.3", - "from": "module-details-from-path@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz" + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=" }, "moment": { "version": "2.23.0", - "from": "moment@>=2.20.0 <3.0.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz" + "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz", + "integrity": "sha1-dZ6kkayX1UusWtd2mW4qWMwbwiU=" }, "moment-timezone": { "version": "0.5.23", - "from": "moment-timezone@>=0.5.14 <0.6.0", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz" + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz", + "integrity": "sha1-fLsA2ywUxxsZMDy0ew+wpthlFGM=", + "requires": { + "moment": ">= 2.9.0" + } }, "ms": { "version": "2.0.0", - "from": "ms@2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mv": { "version": "2.1.1", - "from": "mv@~2", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, "dependencies": { "glob": { "version": "6.0.4", - "from": "glob@>=6.0.1 <7.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "optional": true + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } }, "mkdirp": { "version": "0.5.1", - "from": "mkdirp@>=0.5.1 <0.6.0", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "optional": true + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + } }, "rimraf": { "version": "2.4.5", - "from": "rimraf@>=2.4.0 <2.5.0", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "optional": true + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } } } }, "mysql": { "version": "2.6.2", - "from": "mysql@2.6.2", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", + "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", + "requires": { + "bignumber.js": "2.0.7", + "readable-stream": "~1.1.13", + "require-all": "~1.0.0" + }, "dependencies": { "bignumber.js": { "version": "2.0.7", - "from": "bignumber.js@2.0.7", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz" + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", + "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "readable-stream": { "version": "1.1.14", - "from": "readable-stream@>=1.1.13 <1.2.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, "nan": { "version": "2.12.0", - "from": "nan@>=2.11.1 <3.0.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz" + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", + "integrity": "sha1-nUQ/214TogdwzF5gLu5ZdgpoWIU=" }, "natives": { "version": "1.1.6", - "from": "natives@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz" + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", + "integrity": "sha1-pgO0pJirdxc2ErnqGs3sTZgPALs=" }, "ncp": { "version": "2.0.0", - "from": "ncp@~2.0.0", "resolved": "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", "optional": true }, "needle": { "version": "2.2.4", - "from": "needle@>=2.2.1 <3.0.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz" + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha1-UZMb/4JTOxkot9HWngHxsA/9Kk4=", + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } }, "negotiator": { "version": "0.6.1", - "from": "negotiator@0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, "nise": { "version": "1.4.8", - "from": "nise@>=1.4.7 <2.0.0", "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.8.tgz", + "integrity": "sha1-zpHDHobPmyxMrEnX/Nf1Z3m/1rA=", + "requires": { + "@sinonjs/formatio": "^3.1.0", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0", + "text-encoding": "^0.6.4" + }, "dependencies": { "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "lolex": { "version": "2.7.5", - "from": "lolex@>=2.3.2 <3.0.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz" + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM=" }, "path-to-regexp": { "version": "1.7.0", - "from": "path-to-regexp@>=1.7.0 <2.0.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz" + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } } } }, "node-fetch": { "version": "2.3.0", - "from": "node-fetch@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz" + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", + "integrity": "sha1-Gh2UC7+5FqHT4CGfA36J5x+MX6U=" }, "node-forge": { "version": "0.7.6", - "from": "node-forge@>=0.7.5 <0.8.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz" + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha1-/fO0GK7h+U8O9kLNY0hsd8qXJKw=" }, "node-pre-gyp": { - "version": "0.10.3", - "from": "node-pre-gyp@>=0.10.3 <0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, "dependencies": { "mkdirp": { "version": "0.5.1", - "from": "mkdirp@>=0.5.1 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } } } }, "nopt": { "version": "4.0.1", - "from": "nopt@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz" + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } }, "npm-bundled": { "version": "1.0.5", - "from": "npm-bundled@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz" + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha1-PBcyt7qTazoQMlrvYWRnwMy8yXk=" }, "npm-packlist": { "version": "1.1.12", - "from": "npm-packlist@>=1.1.6 <2.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz" + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", + "integrity": "sha1-Ir3i68EucspIKr1nr8UetJN3JDo=", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } }, "npmlog": { "version": "4.1.2", - "from": "npmlog@>=4.0.2 <5.0.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz" + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } }, "number-is-nan": { "version": "1.0.1", - "from": "number-is-nan@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.9.0", - "from": "oauth-sign@>=0.9.0 <0.10.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" }, "object-assign": { "version": "4.1.1", - "from": "object-assign@>=4.1.0 <5.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "on-finished": { "version": "2.3.0", - "from": "on-finished@>=2.3.0 <2.4.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } }, "once": { "version": "1.4.0", - "from": "once@>=1.3.1 <2.0.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } }, "os-homedir": { "version": "1.0.2", - "from": "os-homedir@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", - "from": "os-tmpdir@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", - "from": "osenv@>=0.1.4 <0.2.0", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz" + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } }, "p-limit": { "version": "2.0.0", - "from": "p-limit@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", + "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "requires": { + "p-try": "^2.0.0" + } }, "p-try": { "version": "2.0.0", - "from": "p-try@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha1-hQgLuHxkaI+keZb+j3376CEXYLE=" }, "parse-duration": { "version": "0.1.1", - "from": "parse-duration@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz" + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz", + "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=" }, "parse-ms": { "version": "2.0.0", - "from": "parse-ms@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.0.0.tgz", + "integrity": "sha1-ezZAKVEAyvP6AQDMzrVmNbYvnWI=" }, "parseurl": { "version": "1.3.2", - "from": "parseurl@>=1.3.2 <1.4.0", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz" + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" }, "path-is-absolute": { "version": "1.0.1", - "from": "path-is-absolute@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { "version": "1.0.6", - "from": "path-parse@>=1.0.6 <2.0.0", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz" + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=" }, "path-to-regexp": { "version": "0.1.7", - "from": "path-to-regexp@0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "pathval": { "version": "1.1.0", - "from": "pathval@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" }, "performance-now": { "version": "2.1.0", - "from": "performance-now@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { "version": "4.0.1", - "from": "pify@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha1-SyzSXFDVmHNcUCkiJP2MbfQeMjE=" }, "pretty-ms": { "version": "4.0.0", - "from": "pretty-ms@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz" + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz", + "integrity": "sha1-Mbr0G5T9AiJwmKqgO9YmCOsNbpI=", + "requires": { + "parse-ms": "^2.0.0" + } }, "process-nextick-args": { "version": "2.0.0", - "from": "process-nextick-args@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" }, "prom-client": { "version": "11.2.0", - "from": "prom-client@>=11.1.3 <12.0.0", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.2.0.tgz" + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.2.0.tgz", + "integrity": "sha512-4gUAq/GR5C8q5eWxOa7tA60AtmkMpbyBd/2btCayvd3h/7HzS0p/kESKRwggJgbFrfdhTCBpOwPAwKiI01Q0VQ==", + "requires": { + "tdigest": "^0.1.1" + } }, "protobufjs": { "version": "6.8.8", - "from": "protobufjs@>=6.8.6 <6.9.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz" + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", + "integrity": "sha1-yLTxKC/XqQ5vWxCe0RyEr4KQjnw=", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + } }, "proxy-addr": { "version": "2.0.4", - "from": "proxy-addr@>=2.0.4 <2.1.0", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz" + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha1-7PxzO/Iv+Mb0B/onUye5q2fki5M=", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } }, "psl": { "version": "1.1.31", - "from": "psl@>=1.1.24 <2.0.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz" + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha1-6aqG0BAbWxBcvpOsa3hM1UcnYYQ=" }, "pump": { "version": "1.0.3", - "from": "pump@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz" + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha1-Xf6DEcM7v2/BgmH580cCxHwIqVQ=", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "punycode": { "version": "2.1.1", - "from": "punycode@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=" }, "qs": { "version": "6.5.2", - "from": "qs@6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=" }, "range-parser": { "version": "1.2.0", - "from": "range-parser@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, "raven": { "version": "1.2.1", - "from": "raven@>=1.1.3 <2.0.0", - "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz" + "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", + "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", + "requires": { + "cookie": "0.3.1", + "json-stringify-safe": "5.0.1", + "lsmod": "1.0.0", + "stack-trace": "0.0.9", + "uuid": "3.0.0" + } }, "raw-body": { "version": "2.3.3", - "from": "raw-body@2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz" + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha1-GzJOzmtXBuFThVvBFIxlu39uoMM=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } }, "rc": { "version": "1.2.8", - "from": "rc@>=1.2.7 <2.0.0", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha1-zZJL9SAKB1uDwYjNa54hG3/A0+0=", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, "dependencies": { "minimist": { "version": "1.2.0", - "from": "minimist@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } }, "readable-stream": { "version": "2.3.6", - "from": "readable-stream@>=2.2.2 <3.0.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz" + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } }, "request": { "version": "2.88.0", - "from": "request@>=2.21.0 <3.0.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, "dependencies": { "uuid": { "version": "3.3.2", - "from": "uuid@>=3.3.2 <4.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" } } }, "require-all": { "version": "1.0.0", - "from": "require-all@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", + "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" }, "require-in-the-middle": { "version": "3.1.0", - "from": "require-in-the-middle@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-3.1.0.tgz" + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-3.1.0.tgz", + "integrity": "sha1-+vS5R8jY0liK5PJGSH4tcmKt0aQ=", + "requires": { + "module-details-from-path": "^1.0.3", + "resolve": "^1.5.0" + } }, "require-like": { "version": "0.1.2", - "from": "require-like@0.1.2", - "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz" + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=" }, "resolve": { "version": "1.9.0", - "from": "resolve@>=1.5.0 <2.0.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz" + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha1-oUxv36j5Kn3x2ZbLcQX6dEZY6gY=", + "requires": { + "path-parse": "^1.0.6" + } }, "retry-as-promised": { "version": "2.3.2", - "from": "retry-as-promised@>=2.3.2 <3.0.0", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.3.2.tgz" + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.3.2.tgz", + "integrity": "sha1-zZdO5P2bX+A8vzGHHuSCIcB3N7c=", + "requires": { + "bluebird": "^3.4.6", + "debug": "^2.6.9" + } }, "retry-axios": { "version": "0.3.2", - "from": "retry-axios@0.3.2", - "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz" + "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz", + "integrity": "sha1-V1fID1hbTMTEmGqi/9R6YMbTXhM=" }, "retry-request": { "version": "4.0.0", - "from": "retry-request@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.0.0.tgz" + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.0.0.tgz", + "integrity": "sha1-XDZhZiebPhDp16oTJ0RnoFy2kpA=", + "requires": { + "through2": "^2.0.0" + } }, "rimraf": { "version": "2.6.2", - "from": "rimraf@>=2.2.8 <3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz" + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "requires": { + "glob": "^7.0.5" + } }, "safe-buffer": { "version": "5.1.2", - "from": "safe-buffer@>=5.1.1 <5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" }, "safe-json-stringify": { "version": "1.2.0", - "from": "safe-json-stringify@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha1-NW5EvJjx+TzkXfFLzXwBzahuCv0=", "optional": true }, "safer-buffer": { "version": "2.1.2", - "from": "safer-buffer@>=2.1.2 <3.0.0", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" }, "sandboxed-module": { "version": "0.3.0", - "from": "sandboxed-module@>=0.3.0 <0.4.0", "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", + "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", "dev": true, + "requires": { + "require-like": "0.1.2", + "stack-trace": "0.0.6" + }, "dependencies": { "stack-trace": { "version": "0.0.6", - "from": "stack-trace@0.0.6", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", + "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", "dev": true } } }, "sax": { "version": "1.2.4", - "from": "sax@>=1.2.4 <2.0.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=" }, "semver": { "version": "5.6.0", - "from": "semver@>=5.5.0 <6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz" + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha1-fnQlb7qknHWqfHogXMInmcrIAAQ=" }, "send": { "version": "0.16.2", - "from": "send@0.16.2", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, "dependencies": { "statuses": { "version": "1.4.0", - "from": "statuses@>=1.4.0 <1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" } } }, "sequelize": { "version": "4.42.0", - "from": "sequelize@>=4.38.0 <5.0.0", "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.42.0.tgz", + "integrity": "sha1-Q5Rnunv+fVr8xW1is+CRhg+/GPM=", + "requires": { + "bluebird": "^3.5.0", + "cls-bluebird": "^2.1.0", + "debug": "^3.1.0", + "depd": "^1.1.0", + "dottie": "^2.0.0", + "generic-pool": "^3.4.0", + "inflection": "1.12.0", + "lodash": "^4.17.1", + "moment": "^2.20.0", + "moment-timezone": "^0.5.14", + "retry-as-promised": "^2.3.2", + "semver": "^5.5.0", + "terraformer-wkt-parser": "^1.1.2", + "toposort-class": "^1.0.1", + "uuid": "^3.2.1", + "validator": "^10.4.0", + "wkx": "^0.4.1" + }, "dependencies": { "debug": { "version": "3.2.6", - "from": "debug@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "requires": { + "ms": "^2.1.1" + } }, "ms": { "version": "2.1.1", - "from": "ms@>=2.1.1 <3.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" }, "uuid": { "version": "3.3.2", - "from": "uuid@>=3.2.1 <4.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" } } }, "serve-static": { "version": "1.13.2", - "from": "serve-static@1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz" + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } }, "set-blocking": { "version": "2.0.0", - "from": "set-blocking@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "setprototypeof": { "version": "1.1.0", - "from": "setprototypeof@1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" }, "settings-sharelatex": { - "version": "1.1.0", + "version": "git+https://github.com/sharelatex/settings-sharelatex.git#93f63d029b52fef8825c3a401b2b6a7ba29b4750", "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", - "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#93f63d029b52fef8825c3a401b2b6a7ba29b4750" + "requires": { + "coffee-script": "1.6.0" + } }, "shimmer": { "version": "1.2.0", - "from": "shimmer@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha1-+Wb3VVeJdj502IQRk2haXnhzZmU=" }, "sigmund": { "version": "1.0.1", - "from": "sigmund@~1.0.0", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" }, "signal-exit": { "version": "3.0.2", - "from": "signal-exit@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz" + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "sinon": { "version": "1.7.3", - "from": "sinon@>=1.7.3 <1.8.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", - "dev": true + "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", + "dev": true, + "requires": { + "buster-format": "~0.5" + } }, "smoke-test-sharelatex": { - "version": "1.0.1", + "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", "from": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "resolved": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", + "requires": { + "mocha": "~1.17.0" + }, "dependencies": { "glob": { "version": "3.2.3", - "from": "glob@3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz" + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "requires": { + "graceful-fs": "~2.0.0", + "inherits": "2", + "minimatch": "~0.2.11" + } }, "graceful-fs": { "version": "2.0.3", - "from": "graceful-fs@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" }, "lru-cache": { "version": "2.7.3", - "from": "lru-cache@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" }, "minimatch": { "version": "0.2.14", - "from": "minimatch@>=0.2.11 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } }, "mocha": { "version": "1.17.1", - "from": "mocha@>=1.17.0 <1.18.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz" + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", + "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", + "requires": { + "commander": "2.0.0", + "debug": "*", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.x", + "jade": "0.26.3", + "mkdirp": "0.3.5" + } } } }, - "sntp": { - "version": "1.0.9", - "from": "sntp@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" - }, "source-map": { "version": "0.6.1", - "from": "source-map@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" }, "split": { "version": "1.0.1", - "from": "split@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", + "requires": { + "through": "2" + } }, "split-ca": { "version": "1.0.1", - "from": "split-ca@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" }, "sqlite3": { - "version": "4.0.4", - "from": "sqlite3@>=4.0.2 <5.0.0", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.4.tgz", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.6.tgz", + "integrity": "sha512-EqBXxHdKiwvNMRCgml86VTL5TK1i0IKiumnfxykX0gh6H6jaKijAXvE9O1N7+omfNSawR2fOmIyJZcfe8HYWpw==", + "requires": { + "nan": "~2.10.0", + "node-pre-gyp": "^0.11.0", + "request": "^2.87.0" + }, "dependencies": { "nan": { "version": "2.10.0", - "from": "nan@>=2.10.0 <2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz" + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" } } }, "sshpk": { "version": "1.15.2", - "from": "sshpk@>=1.7.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz" + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha1-yUbWvZsaOdDoY1dj9SQtbtbctik=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } }, "stack-trace": { "version": "0.0.9", - "from": "stack-trace@0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz" + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" }, "statsd-parser": { "version": "0.0.4", - "from": "statsd-parser@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz" + "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", + "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" }, "statuses": { "version": "1.5.0", - "from": "statuses@>=1.4.0 <2.0.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, "stream-shift": { "version": "1.0.0", - "from": "stream-shift@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz" - }, - "string_decoder": { - "version": "1.1.1", - "from": "string_decoder@>=1.1.1 <1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, "string-width": { "version": "1.0.2", - "from": "string-width@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } }, - "stringstream": { - "version": "0.0.6", - "from": "stringstream@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz" + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "requires": { + "safe-buffer": "~5.1.0" + } }, "strip-ansi": { "version": "3.0.1", - "from": "strip-ansi@>=3.0.1 <4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } }, "strip-json-comments": { "version": "2.0.1", - "from": "strip-json-comments@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supports-color": { "version": "4.4.0", - "from": "supports-color@4.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "dev": true + "integrity": "sha1-iD992rwWUUKyphQn8zUt7RldGj4=", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } }, "symbol-observable": { "version": "1.2.0", - "from": "symbol-observable@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha1-wiaIrtTqs83C3+rLtWFmBWCgCAQ=" }, "tar": { "version": "4.4.8", - "from": "tar@>=4.0.0 <5.0.0", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha1-sZ7sP94qluZGZt+f20DFyhvDdH0=", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + }, "dependencies": { "mkdirp": { "version": "0.5.1", - "from": "mkdirp@>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } } } }, "tar-fs": { "version": "1.16.3", - "from": "tar-fs@>=1.16.3 <1.17.0", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha1-lmpiiEHaLEAQQGqCFny9Xgxy1Qk=", + "requires": { + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" + }, "dependencies": { "mkdirp": { "version": "0.5.1", - "from": "mkdirp@>=0.5.1 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - } - } - }, - "tar-pack": { - "version": "3.4.1", - "from": "tar-pack@>=3.4.0 <4.0.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "dependencies": { - "tar": { - "version": "2.2.1", - "from": "tar@>=2.2.1 <3.0.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } } } }, "tar-stream": { "version": "1.6.2", - "from": "tar-stream@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz" + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } }, "tdigest": { "version": "0.1.1", - "from": "tdigest@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz" + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } }, "teeny-request": { "version": "3.11.3", - "from": "teeny-request@>=3.11.1 <4.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "integrity": "sha1-M1xin3ZF5dZZk2LfLzIwxMvCOlU=", + "requires": { + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0", + "uuid": "^3.3.2" + }, "dependencies": { "uuid": { "version": "3.3.2", - "from": "uuid@>=3.3.2 <4.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz" + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" } } }, "terraformer": { "version": "1.0.9", - "from": "terraformer@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.9.tgz" + "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.9.tgz", + "integrity": "sha1-d4Uf70pJyQs0XcU88mgJ/fKdzaY=", + "requires": { + "@types/geojson": "^1.0.0" + } }, "terraformer-wkt-parser": { "version": "1.2.0", - "from": "terraformer-wkt-parser@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz", + "integrity": "sha1-ydasPf8l9MC9NE6WH0JpSWGDTDQ=", + "requires": { + "@types/geojson": "^1.0.0", + "terraformer": "~1.0.5" + } }, "text-encoding": { "version": "0.6.4", - "from": "text-encoding@>=0.6.4 <0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz" + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=" }, "through": { "version": "2.3.8", - "from": "through@>=2.2.7 <3.0.0", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.5", - "from": "through2@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha1-AcHjnrMdB8t9A6lqcIIyYLIxMs0=", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } }, "timekeeper": { "version": "0.0.4", - "from": "timekeeper@0.0.4", "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", + "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", "dev": true }, "to-buffer": { "version": "1.1.1", - "from": "to-buffer@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz" + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=" }, "toposort-class": { "version": "1.0.1", - "from": "toposort-class@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" }, "tough-cookie": { "version": "2.4.3", - "from": "tough-cookie@>=2.4.3 <2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, "dependencies": { "punycode": { "version": "1.4.1", - "from": "punycode@>=1.4.1 <2.0.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" } } }, "tunnel-agent": { "version": "0.6.0", - "from": "tunnel-agent@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } }, "tweetnacl": { "version": "0.14.5", - "from": "tweetnacl@>=0.14.0 <0.15.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-detect": { "version": "0.1.1", - "from": "type-detect@0.1.1", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", "dev": true }, "type-is": { "version": "1.6.16", - "from": "type-is@>=1.6.16 <1.7.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz" + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } }, "typedarray": { "version": "0.0.6", - "from": "typedarray@>=0.0.6 <0.0.7", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" - }, - "uid-number": { - "version": "0.0.6", - "from": "uid-number@>=0.0.6 <0.0.7", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "underscore": { "version": "1.9.1", - "from": "underscore@>=1.8.2 <2.0.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz" + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE=" }, "underscore.string": { "version": "2.2.1", - "from": "underscore.string@>=2.2.1 <2.3.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz" + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=" }, "unpipe": { "version": "1.0.0", - "from": "unpipe@1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "uri-js": { "version": "4.2.2", - "from": "uri-js@>=4.2.2 <5.0.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz" + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", + "requires": { + "punycode": "^2.1.0" + } }, "util-deprecate": { "version": "1.0.2", - "from": "util-deprecate@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", - "from": "utils-merge@1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "3.0.0", - "from": "uuid@3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz" - }, - "v8-profiler": { - "version": "5.7.0", - "from": "v8-profiler@>=5.2.4 <6.0.0", - "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", + "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + }, + "v8-profiler-node8": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/v8-profiler-node8/-/v8-profiler-node8-6.0.1.tgz", + "integrity": "sha512-DD7L0c/2KeFFQxs20VaFPHq/CSintttW4YB+QJdJ5ZohxOegbsMCvnZKHRHVA9UZfVYqq6ZsLaywe74+3JpZjw==", + "requires": { + "nan": "^2.5.1", + "node-pre-gyp": "^0.11.0" + }, "dependencies": { - "ajv": { - "version": "4.11.8", - "from": "ajv@>=4.9.1 <5.0.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz" - }, - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" - }, - "aws-sign2": { - "version": "0.6.0", - "from": "aws-sign2@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" - }, - "form-data": { - "version": "2.1.4", - "from": "form-data@>=2.1.1 <2.2.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz" - }, - "har-schema": { - "version": "1.0.5", - "from": "har-schema@>=1.0.5 <2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz" - }, - "har-validator": { - "version": "4.2.1", - "from": "har-validator@>=4.2.1 <4.3.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz" - }, - "http-signature": { - "version": "1.1.1", - "from": "http-signature@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" - }, "mkdirp": { "version": "0.5.1", - "from": "mkdirp@>=0.5.1 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } }, "node-pre-gyp": { - "version": "0.6.39", - "from": "node-pre-gyp@>=0.6.34 <0.7.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz" - }, - "oauth-sign": { - "version": "0.8.2", - "from": "oauth-sign@>=0.8.1 <0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" - }, - "performance-now": { - "version": "0.2.0", - "from": "performance-now@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz" - }, - "punycode": { - "version": "1.4.1", - "from": "punycode@>=1.4.1 <2.0.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - }, - "qs": { - "version": "6.4.0", - "from": "qs@>=6.4.0 <6.5.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz" - }, - "request": { - "version": "2.81.0", - "from": "request@2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz" - }, - "tar": { - "version": "2.2.1", - "from": "tar@>=2.2.1 <3.0.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" - }, - "tough-cookie": { - "version": "2.3.4", - "from": "tough-cookie@>=2.3.0 <2.4.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz" + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } } } }, "validator": { "version": "10.9.0", - "from": "validator@>=10.4.0 <11.0.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.9.0.tgz" + "resolved": "https://registry.npmjs.org/validator/-/validator-10.9.0.tgz", + "integrity": "sha1-0QwRZztQYft8z0wRFEEkEbK6wqg=" }, "vary": { "version": "1.1.2", - "from": "vary@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "verror": { "version": "1.10.0", - "from": "verror@1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } }, "which": { "version": "1.0.9", - "from": "which@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=" }, "wide-align": { "version": "1.1.3", - "from": "wide-align@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz" + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=", + "requires": { + "string-width": "^1.0.2 || 2" + } }, "wkx": { "version": "0.4.6", - "from": "wkx@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.6.tgz" + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.6.tgz", + "integrity": "sha1-Ioq1kuZFc4Lqb7efyCUFjQf85SM=", + "requires": { + "@types/node": "*" + } }, "wrappy": { "version": "1.0.2", - "from": "wrappy@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "wrench": { "version": "1.5.9", - "from": "wrench@>=1.5.4 <1.6.0", - "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz" + "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", + "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" }, "xtend": { "version": "4.0.1", - "from": "xtend@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "yallist": { "version": "3.0.3", - "from": "yallist@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha1-tLBJ4xS+VF486AIjbWzSLNkcPek=" } } } diff --git a/package.json b/package.json index 065cdce6..416dd3e3 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,15 @@ "sequelize": "^4.38.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "^4.0.2", + "sqlite3": "^4.0.6", "underscore": "^1.8.2", - "v8-profiler": "^5.2.4", + "v8-profiler-node8": "^6.0.1", "wrench": "~1.5.4" }, "devDependencies": { "bunyan": "^0.22.1", "chai": "~1.8.1", - "coffee-script": "1.6.0", + "coffeescript": "1.6.0", "mocha": "^4.0.1", "sandboxed-module": "~0.3.0", "sinon": "~1.7.3", diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index d4bd19f5..9f96e6d6 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -33,7 +33,10 @@ compare = (originalPath, generatedPath, callback = (error, same) ->) -> proc.stderr.on "data", (chunk) -> stderr += chunk proc.on "exit", () -> if stderr.trim() == "0 (0)" - fs.unlink diff_file # remove output diff if test matches expected image + # remove output diff if test matches expected image + fs.unlink diff_file, (err) -> + if err + throw err callback null, true else console.log "compare result", stderr From a194d7ad05185924396ed2de6d333b9871ce316c Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 17 Dec 2018 19:21:53 +0000 Subject: [PATCH 437/709] Fix broken spacing --- app/coffee/RequestParser.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index cabbac38..b887ed30 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -87,7 +87,7 @@ module.exports = RequestParser = throw "resource modified date could not be understood: #{resource.modified}" if !resource.url? and !resource.content? - throw "all resources should have either a url or content attribute" + throw "all resources should have either a url or content attribute" if resource.content? and typeof resource.content != "string" throw "content attribute should be a string" if resource.url? and typeof resource.url != "string" From 12fee9e4dfc610343e1342dcb9b0775d619ae1f4 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 24 Jan 2019 12:30:37 +0000 Subject: [PATCH 438/709] add epoll_pwait to secomp profile Last year golang changed from epoll_wait to epoll_pwait https://github.com/golang/go/issues/23750 This causes golang panic errors on mac when running secomp secure compiles using docker 18.09.1. It may start to become a problem on linux where we are running on 17.03.2-ce in production. --- seccomp/clsi-profile.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seccomp/clsi-profile.json b/seccomp/clsi-profile.json index 34fd2520..e7e9dd01 100644 --- a/seccomp/clsi-profile.json +++ b/seccomp/clsi-profile.json @@ -827,6 +827,10 @@ "name": "gettimeofday", "action": "SCMP_ACT_ALLOW", "args": [] + }, { + "name": "epoll_pwait", + "action": "SCMP_ACT_ALLOW", + "args": [] } ] } \ No newline at end of file From d20856f7992623f2b70d79cd4dbb15a6dacfde0c Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 12 Feb 2019 16:54:59 +0000 Subject: [PATCH 439/709] use explicit json content-type to avoid security issues with text/html --- app/coffee/CompileController.coffee | 6 +++--- test/unit/coffee/CompileControllerTests.coffee | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 0a51c947..73a7fd1a 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -84,7 +84,7 @@ module.exports = CompileController = user_id = req.params.user_id CompileManager.syncFromCode project_id, user_id, file, line, column, (error, pdfPositions) -> return next(error) if error? - res.send JSON.stringify { + res.json { pdf: pdfPositions } @@ -96,7 +96,7 @@ module.exports = CompileController = user_id = req.params.user_id CompileManager.syncFromPdf project_id, user_id, page, h, v, (error, codePositions) -> return next(error) if error? - res.send JSON.stringify { + res.json { code: codePositions } @@ -109,7 +109,7 @@ module.exports = CompileController = CompileManager.wordcount project_id, user_id, file, image, (error, result) -> return next(error) if error? - res.send JSON.stringify { + res.json { texcount: result } diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index 529c7322..034adfcd 100644 --- a/test/unit/coffee/CompileControllerTests.coffee +++ b/test/unit/coffee/CompileControllerTests.coffee @@ -144,7 +144,7 @@ describe "CompileController", -> file: @file line: @line.toString() column: @column.toString() - @res.send = sinon.stub() + @res.json = sinon.stub() @CompileManager.syncFromCode = sinon.stub().callsArgWith(5, null, @pdfPositions = ["mock-positions"]) @CompileController.syncFromCode @req, @res, @next @@ -155,8 +155,8 @@ describe "CompileController", -> .should.equal true it "should return the positions", -> - @res.send - .calledWith(JSON.stringify + @res.json + .calledWith( pdf: @pdfPositions ) .should.equal true @@ -173,7 +173,7 @@ describe "CompileController", -> page: @page.toString() h: @h.toString() v: @v.toString() - @res.send = sinon.stub() + @res.json = sinon.stub() @CompileManager.syncFromPdf = sinon.stub().callsArgWith(5, null, @codePositions = ["mock-positions"]) @CompileController.syncFromPdf @req, @res, @next @@ -184,8 +184,8 @@ describe "CompileController", -> .should.equal true it "should return the positions", -> - @res.send - .calledWith(JSON.stringify + @res.json + .calledWith( code: @codePositions ) .should.equal true @@ -199,7 +199,7 @@ describe "CompileController", -> @req.query = file: @file image: @image = "example.com/image" - @res.send = sinon.stub() + @res.json = sinon.stub() @CompileManager.wordcount = sinon.stub().callsArgWith(4, null, @texcount = ["mock-texcount"]) @CompileController.wordcount @req, @res, @next @@ -210,8 +210,8 @@ describe "CompileController", -> .should.equal true it "should return the texcount info", -> - @res.send - .calledWith(JSON.stringify + @res.json + .calledWith( texcount: @texcount ) .should.equal true From 1899d277321ca27b59a06e9cf8c8592c45f39b0a Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 22 Feb 2019 13:23:26 +0000 Subject: [PATCH 440/709] increase acceptance test timeout to 1 minute --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 416dd3e3..4ae61ced 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 60000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", From e2377e1c1c4cf52ca7da52ecec7c9e07ddf06b55 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Mon, 4 Mar 2019 12:05:28 +0000 Subject: [PATCH 441/709] Bump logger to v1.6.0 --- npm-shrinkwrap.json | 690 +------------------------------------------- package.json | 2 +- 2 files changed, 9 insertions(+), 683 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9fe83ed3..5a2c4dc9 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -221,39 +221,6 @@ "symbol-observable": "^1.2.0" } }, - "@sinonjs/commons": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz", - "integrity": "sha1-UKJ1QBa28wqZTO2m2aCow2rdqEk=", - "requires": { - "type-detect": "4.0.8" - }, - "dependencies": { - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=" - } - } - }, - "@sinonjs/formatio": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.1.0.tgz", - "integrity": "sha1-asnR6xghmE2ExJlnJuRdFkbYzOU=", - "requires": { - "@sinonjs/samsam": "^2 || ^3" - } - }, - "@sinonjs/samsam": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.0.2.tgz", - "integrity": "sha1-ME+zO9VYWgst+KTIAfy0f6hNjkM=", - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash.get": "^4.4.2" - } - }, "@types/caseless": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", @@ -368,11 +335,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, - "ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=" - }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -387,37 +349,11 @@ "readable-stream": "^2.0.6" } }, - "argparse": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", - "requires": { - "underscore": "~1.7.0", - "underscore.string": "~2.4.0" - }, - "dependencies": { - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" - }, - "underscore.string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=" - } - } - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" - }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -633,43 +569,6 @@ "deep-eql": "0.1.3" } }, - "chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "requires": { - "ansi-styles": "^1.1.0", - "escape-string-regexp": "^1.0.0", - "has-ansi": "^0.1.0", - "strip-ansi": "^0.3.0", - "supports-color": "^0.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=" - }, - "strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "requires": { - "ansi-regex": "^0.2.1" - } - }, - "supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=" - } - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" - }, "chownr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", @@ -700,11 +599,6 @@ "integrity": "sha1-bdTeHrYveE2MjYCWdVLLpUf/2d4=", "dev": true }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" - }, "combined-stream": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", @@ -786,11 +680,6 @@ "assert-plus": "^1.0.0" } }, - "dateformat": { - "version": "1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -997,28 +886,14 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=" - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -1109,45 +984,6 @@ "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz", "integrity": "sha1-WKRmaX34piBc39vzlVNri9d3pfY=" }, - "findup-sync": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", - "requires": { - "glob": "~3.2.9", - "lodash": "~2.4.1" - }, - "dependencies": { - "glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "requires": { - "inherits": "2", - "minimatch": "0.3" - } - }, - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "minimatch": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "requires": { - "lru-cache": "2", - "sigmund": "~1.0.0" - } - } - } - }, "follow-redirects": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", @@ -1258,16 +1094,6 @@ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz", "integrity": "sha1-kv9xllINZwg5pnMICSoSqt8valk=" }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" - }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=" - }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1337,325 +1163,6 @@ "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" }, - "grunt": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", - "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", - "requires": { - "async": "~0.1.22", - "coffee-script": "~1.3.3", - "colors": "~0.6.2", - "dateformat": "1.0.2-1.2.3", - "eventemitter2": "~0.4.13", - "exit": "~0.1.1", - "findup-sync": "~0.1.2", - "getobject": "~0.1.0", - "glob": "~3.1.21", - "grunt-legacy-log": "~0.1.0", - "grunt-legacy-util": "~0.2.0", - "hooker": "~0.2.3", - "iconv-lite": "~0.2.11", - "js-yaml": "~2.0.5", - "lodash": "~0.9.2", - "minimatch": "~0.2.12", - "nopt": "~1.0.10", - "rimraf": "~2.2.8", - "underscore.string": "~2.2.1", - "which": "~1.0.5" - }, - "dependencies": { - "async": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=" - }, - "coffee-script": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", - "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=" - }, - "glob": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", - "requires": { - "graceful-fs": "~1.2.0", - "inherits": "1", - "minimatch": "~0.2.11" - } - }, - "graceful-fs": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=" - }, - "iconv-lite": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=" - }, - "inherits": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=" - }, - "lodash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=" - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "requires": { - "lru-cache": "2", - "sigmund": "~1.0.0" - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "requires": { - "abbrev": "1" - } - }, - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" - } - } - }, - "grunt-bunyan": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/grunt-bunyan/-/grunt-bunyan-0.5.0.tgz", - "integrity": "sha1-aCnXbgGZQ9owQTk2MaNuKsgpsWw=", - "requires": { - "lodash": "~2.4.1" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - } - } - }, - "grunt-contrib-clean": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.6.0.tgz", - "integrity": "sha1-9TLbpLghJnTHwBPhRr2mY4uQSPY=", - "requires": { - "rimraf": "~2.2.1" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" - } - } - }, - "grunt-contrib-coffee": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", - "integrity": "sha1-+v48nuikQryNF9WlwZ/I5i2fP0U=", - "requires": { - "chalk": "~0.5.0", - "coffee-script": "~1.7.0", - "lodash": "~2.4.1" - }, - "dependencies": { - "coffee-script": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", - "integrity": "sha1-YplqhheAx15tUGnROCJyO3NAS/w=", - "requires": { - "mkdirp": "~0.3.5" - } - }, - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - } - } - }, - "grunt-execute": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz", - "integrity": "sha1-TpRf5XlZzA3neZCDtrQq7ZYWNQo=" - }, - "grunt-legacy-log": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", - "requires": { - "colors": "~0.6.2", - "grunt-legacy-log-utils": "~0.1.1", - "hooker": "~0.2.3", - "lodash": "~2.4.1", - "underscore.string": "~2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=" - } - } - }, - "grunt-legacy-log-utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", - "requires": { - "colors": "~0.6.2", - "lodash": "~2.4.1", - "underscore.string": "~2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=" - } - } - }, - "grunt-legacy-util": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", - "requires": { - "async": "~0.1.22", - "exit": "~0.1.1", - "getobject": "~0.1.0", - "hooker": "~0.2.3", - "lodash": "~0.9.2", - "underscore.string": "~2.2.1", - "which": "~1.0.5" - }, - "dependencies": { - "async": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=" - }, - "lodash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=" - } - } - }, - "grunt-mocha-test": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.11.0.tgz", - "integrity": "sha1-deQboQdZDkrL0phgklwBLkQ8oyI=", - "requires": { - "fs-extra": "~0.9.1", - "hooker": "~0.2.3", - "mocha": "~1.20.0" - }, - "dependencies": { - "fs-extra": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.9.1.tgz", - "integrity": "sha1-h9v8ATg6jdzn2dVJbzYIVkiJ8VY=", - "requires": { - "jsonfile": "~1.1.0", - "mkdirp": "^0.5.0", - "ncp": "^0.5.1", - "rimraf": "^2.2.8" - } - }, - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "requires": { - "graceful-fs": "~2.0.0", - "inherits": "2", - "minimatch": "~0.2.11" - } - }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" - }, - "jsonfile": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.1.1.tgz", - "integrity": "sha1-2k/WrXfxolUgPqY8e8Mtwx72RDM=" - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "requires": { - "lru-cache": "2", - "sigmund": "~1.0.0" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.20.1.tgz", - "integrity": "sha1-80ODLZ/gx9l8ZPxwRI9RNt+f7Vs=", - "requires": { - "commander": "2.0.0", - "debug": "*", - "diff": "1.0.7", - "glob": "3.2.3", - "growl": "1.7.x", - "jade": "0.26.3", - "mkdirp": "0.3.5" - }, - "dependencies": { - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" - } - } - }, - "ncp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", - "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=" - } - } - }, "gtoken": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.0.tgz", @@ -1694,21 +1201,6 @@ "har-schema": "^2.0.0" } }, - "has-ansi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", - "requires": { - "ansi-regex": "^0.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=" - } - } - }, "has-flag": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", @@ -1739,11 +1231,6 @@ "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.1.tgz", "integrity": "sha512-E1nK1KCHaX9NvY3wkCMpZxj3oGokO5fgjcKUBaOgSkkvNogm8ngb8isKtzlxnLT37/JXLODVYXz9ti99Bxz8gg==" }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=" - }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -1893,15 +1380,6 @@ } } }, - "js-yaml": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", - "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", - "requires": { - "argparse": "~ 0.1.11", - "esprima": "~ 1.0.2" - } - }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -1962,11 +1440,6 @@ "verror": "1.10.0" } }, - "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=" - }, "jwa": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", @@ -1999,40 +1472,21 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, "lodash.pickby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=" }, "logger-sharelatex": { - "version": "git+https://github.com/sharelatex/logger-sharelatex.git#e8e1b95052f62e107336053e4a983f81cdbdf589", - "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.9", + "version": "git+https://github.com/sharelatex/logger-sharelatex.git#8359f16a1546c481a5d1cde63de5b5b6bb569789", + "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.6.0", "requires": { "bunyan": "1.5.1", - "chai": "^4.2.0", "coffee-script": "1.12.4", - "grunt": "^0.4.5", - "grunt-bunyan": "^0.5.0", - "grunt-contrib-clean": "^0.6.0", - "grunt-contrib-coffee": "^0.11.0", - "grunt-execute": "^0.2.2", - "grunt-mocha-test": "^0.11.0", "raven": "^1.1.3", - "sandboxed-module": "^2.0.3", - "sinon": "^7.2.2", - "timekeeper": "^1.0.0" + "request": "^2.88.0" }, "dependencies": { - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs=" - }, "bunyan": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", @@ -2043,37 +1497,11 @@ "safe-json-stringify": "~1" } }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha1-dgqnLPION5XoSxKHfODoNzeqKeU=", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, "coffee-script": { "version": "1.12.4", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.4.tgz", "integrity": "sha1-/hvO2X/h+zknuZjytFYW4GWL4f8=" }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha1-38lARACtHI/gI+faHfHBR8S0RN8=", - "requires": { - "type-detect": "^4.0.0" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=" - }, "dtrace-provider": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", @@ -2082,60 +1510,9 @@ "requires": { "nan": "^2.0.8" } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "sandboxed-module": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-2.0.3.tgz", - "integrity": "sha1-x+VFkzm7y6KMUwPusz9ug4e/upY=", - "requires": { - "require-like": "0.1.2", - "stack-trace": "0.0.9" - } - }, - "sinon": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz", - "integrity": "sha1-OI7KvUL6k8WSv8cdNacIlNWgygc=", - "requires": { - "@sinonjs/commons": "^1.2.0", - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/samsam": "^3.0.2", - "diff": "^3.5.0", - "lolex": "^3.0.0", - "nise": "^1.4.7", - "supports-color": "^5.5.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", - "requires": { - "has-flag": "^3.0.0" - } - }, - "timekeeper": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-1.0.0.tgz", - "integrity": "sha1-Lziu4elLEd1m2FgP8aqdzGoroNg=" - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=" } } }, - "lolex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz", - "integrity": "sha1-8E7hqKoT9g8avXsOj0IT7HLsGT4=" - }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -2476,38 +1853,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, - "nise": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.8.tgz", - "integrity": "sha1-zpHDHobPmyxMrEnX/Nf1Z3m/1rA=", - "requires": { - "@sinonjs/formatio": "^3.1.0", - "just-extend": "^4.0.2", - "lolex": "^2.3.2", - "path-to-regexp": "^1.7.0", - "text-encoding": "^0.6.4" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM=" - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "requires": { - "isarray": "0.0.1" - } - } - } - }, "node-fetch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", @@ -2672,11 +2017,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" - }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -2872,7 +2212,8 @@ "require-like": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=" + "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", + "dev": true }, "resolve": { "version": "1.9.0", @@ -2920,7 +2261,7 @@ "safe-json-stringify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", - "integrity": "sha1-NW5EvJjx+TzkXfFLzXwBzahuCv0=", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", "optional": true }, "safer-buffer": { @@ -3348,11 +2689,6 @@ "terraformer": "~1.0.5" } }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=" - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -3437,11 +2773,6 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE=" }, - "underscore.string": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=" - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3526,11 +2857,6 @@ "extsprintf": "^1.2.0" } }, - "which": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", - "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=" - }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index 4ae61ced..b2cd6713 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "fs-extra": "^0.16.3", "heapdump": "^0.3.5", "lockfile": "^1.0.3", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.9", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.6.0", "lynx": "0.0.11", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.12", "mkdirp": "0.3.5", From 31153c479cc85320b60601970dddda581d900441 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 22 Mar 2019 20:42:06 +0000 Subject: [PATCH 442/709] change console.log for logger.log --- app/coffee/DbQueue.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/DbQueue.coffee b/app/coffee/DbQueue.coffee index 0e025906..a3593fdd 100644 --- a/app/coffee/DbQueue.coffee +++ b/app/coffee/DbQueue.coffee @@ -1,12 +1,12 @@ async = require "async" Settings = require "settings-sharelatex" - +logger = require("logger-sharelatex") queue = async.queue((task, cb)-> task(cb) , Settings.parallelSqlQueryLimit) queue.drain = ()-> - console.log('HI all items have been processed') + logger.debug('all items have been processed') module.exports = queue: queue From 9cb14660d46d0f6d51b774de9cba4224416ef357 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 26 Mar 2019 11:50:59 +0000 Subject: [PATCH 443/709] Formalise node 10.15 update --- .nvmrc | 2 +- buildscript.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.nvmrc b/.nvmrc index d36e8d82..f9fb144f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.15.1 +10.15.0 diff --git a/buildscript.txt b/buildscript.txt index abdf7ed9..2ca09a3f 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,6 +1,6 @@ --script-version=1.1.11 clsi ---node-version=6.15.1 +--node-version=10.15.0 --acceptance-creds=None --language=coffeescript --dependencies=mongo,redis From bd42fe577676f13997cf28c9a91b6da0ae0b111d Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 1 Apr 2019 09:42:54 +0100 Subject: [PATCH 444/709] increase timeout for long-running acceptance tests --- test/acceptance/coffee/ExampleDocumentTests.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.coffee index 9f96e6d6..f8e4a777 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -15,6 +15,8 @@ try catch err console.log err, fixturePath("tmp"), "unable to create fixture tmp path" +MOCHA_LATEX_TIMEOUT = 60 * 1000 + convertToPng = (pdfPath, pngPath, callback = (error) ->) -> command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" console.log "COMMAND" @@ -109,6 +111,7 @@ describe "Example Documents", -> @project_id = Client.randomId() + "_" + example_dir it "should generate the correct pdf", (done) -> + this.timeout(MOCHA_LATEX_TIMEOUT) Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => if error || body?.compile?.status is "failure" console.log "DEBUG: error", error, "body", JSON.stringify(body) @@ -116,6 +119,7 @@ describe "Example Documents", -> downloadAndComparePdf(@project_id, example_dir, pdf.url, done) it "should generate the correct pdf on the second run as well", (done) -> + this.timeout(MOCHA_LATEX_TIMEOUT) Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => if error || body?.compile?.status is "failure" console.log "DEBUG: error", error, "body", JSON.stringify(body) From adfeffd254edc6c5b671f3c39becdda837d208c6 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Mon, 22 Apr 2019 01:38:24 +0200 Subject: [PATCH 445/709] [docker] add support for a different docker group id on the docker host Signed-off-by: Jakob Ackermann <das7pad@outlook.com> --- entrypoint.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index ea295c7e..71ced141 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,8 +4,10 @@ echo "Changing permissions of /var/run/docker.sock for sibling containers" ls -al /var/run/docker.sock docker --version cat /etc/passwd -usermod -aG docker node -chown root:docker /var/run/docker.sock + +DOCKER_GROUP=$(stat -c '%g' /var/run/docker.sock) +groupadd --non-unique --gid ${DOCKER_GROUP} dockeronhost +usermod -aG dockeronhost node mkdir -p /app/cache chown -R node:node /app/cache From d2c2629ef53a62b847bfabdead0c2d8b2b3aaf22 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 2 May 2019 15:27:21 +0100 Subject: [PATCH 446/709] Bump buildscripts from 1.1.11 to 1.1.20 --- Jenkinsfile | 10 ++++++++-- Makefile | 10 +++++++--- buildscript.txt | 5 ++--- docker-compose.ci.yml | 15 ++++++++++++++- docker-compose.yml | 16 +++++++++++++++- package.json | 2 +- 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d82360d4..e5b9ce6e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -48,8 +48,11 @@ pipeline { } } - stage('Package and publish build') { + stage('Package and docker push') { steps { + sh 'echo ${BUILD_NUMBER} > build_number.txt' + sh 'touch build.tar.gz' // Avoid tar warning about files changing during read + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make tar' withCredentials([file(credentialsId: 'gcr.io_overleaf-ops', variable: 'DOCKER_REPO_KEY_PATH')]) { sh 'docker login -u _json_key --password-stdin https://gcr.io/overleaf-ops < ${DOCKER_REPO_KEY_PATH}' @@ -60,9 +63,12 @@ pipeline { } } - stage('Publish build number') { + stage('Publish to s3') { steps { sh 'echo ${BRANCH_NAME}-${BUILD_NUMBER} > build_number.txt' + withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { + s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") + } withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { // The deployment process uses this file to figure out the latest build s3Upload(file:'build_number.txt', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/latest") diff --git a/Makefile b/Makefile index e7a1e341..ad832318 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.11 +# Version: 1.1.20 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -13,7 +13,6 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ MOCHA_GREP=${MOCHA_GREP} \ docker-compose ${DOCKER_COMPOSE_FLAGS} - clean: docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) @@ -27,7 +26,9 @@ test: test_unit test_acceptance test_unit: @[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit -test_acceptance: test_clean test_acceptance_pre_run # clear the database before each acceptance test run +test_acceptance: test_clean test_acceptance_pre_run test_acceptance_run + +test_acceptance_run: @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance test_clean: @@ -40,6 +41,9 @@ build: --tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ . +tar: + $(DOCKER_COMPOSE) up tar + publish: docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) diff --git a/buildscript.txt b/buildscript.txt index 2ca09a3f..085d8cb8 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,9 +1,8 @@ ---script-version=1.1.11 clsi +--language=coffeescript --node-version=10.15.0 --acceptance-creds=None ---language=coffeescript --dependencies=mongo,redis --docker-repos=gcr.io/overleaf-ops ---kube=false --build-target=docker +--script-version=1.1.20 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 698c5cb1..c6c35dba 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.11 +# Version: 1.1.20 version: "2" @@ -9,6 +9,8 @@ services: test_unit: image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER command: npm run test:unit:_run + environment: + NODE_ENV: test test_acceptance: @@ -23,11 +25,22 @@ services: MONGO_HOST: mongo POSTGRES_HOST: postgres MOCHA_GREP: ${MOCHA_GREP} + NODE_ENV: test depends_on: - mongo - redis command: npm run test:acceptance:_run + + + tar: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + volumes: + - ./:/tmp/build/ + command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . + user: root + redis: image: redis diff --git a/docker-compose.yml b/docker-compose.yml index 5c3c4c86..3af0f7a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.11 +# Version: 1.1.20 version: "2" @@ -13,6 +13,7 @@ services: working_dir: /app environment: MOCHA_GREP: ${MOCHA_GREP} + NODE_ENV: test command: npm run test:unit test_acceptance: @@ -29,14 +30,27 @@ services: MONGO_HOST: mongo POSTGRES_HOST: postgres MOCHA_GREP: ${MOCHA_GREP} + LOG_LEVEL: ERROR + NODE_ENV: test depends_on: - mongo - redis command: npm run test:acceptance + + + tar: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + volumes: + - ./:/tmp/build/ + command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . + user: root + redis: image: redis mongo: image: mongo:3.4 + diff --git a/package.json b/package.json index b2cd6713..61df3979 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 60000 --exit $@ test/acceptance/js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", From 11cf8a98faee66e6acbddd3f2cb9ec4766db5644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Alby?= <tim.alby@overleaf.com> Date: Tue, 7 May 2019 16:41:17 +0100 Subject: [PATCH 447/709] Update README.md --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba0a63e4..5ae02a86 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -clsi-sharelatex +overleaf/clsi =============== A web api for compiling LaTeX documents in the cloud -[![Build Status](https://travis-ci.org/sharelatex/clsi-sharelatex.png?branch=master)](https://travis-ci.org/sharelatex/clsi-sharelatex) - The Common LaTeX Service Interface (CLSI) provides a RESTful interface to traditional LaTeX tools (or, more generally, any command line tool for composing marked-up documents into a display format such as PDF or HTML). The CLSI listens on the following ports by default: * TCP/3009 - the RESTful interface @@ -32,9 +30,9 @@ The CLSI can be configured through the following environment variables: Installation ------------ -The CLSI can be installed and set up as part of the entire [ShareLaTeX stack](https://github.com/sharelatex/sharelatex) (complete with front end editor and document storage), or it can be run as a standalone service. To run is as a standalone service, first checkout this repository: +The CLSI can be installed and set up as part of the entire [Overleaf stack](https://github.com/overleaf/overleaf) (complete with front end editor and document storage), or it can be run as a standalone service. To run is as a standalone service, first checkout this repository: - $ git clone git@github.com:sharelatex/clsi-sharelatex.git + $ git clone git@github.com:overleaf/clsi.git Then install the require npm modules: @@ -116,4 +114,4 @@ License The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the `LICENSE` file. -Copyright (c) ShareLaTeX, 2014. +Copyright (c) Overleaf, 2014-2019. From 03047f45afa64f002cb6b387c6d5260666c3f967 Mon Sep 17 00:00:00 2001 From: Tim Alby <timothee.alby@gmail.com> Date: Tue, 7 May 2019 18:31:54 +0200 Subject: [PATCH 448/709] update Git URL in Jenkinsfile --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d82360d4..f5b5f1df 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,10 +4,10 @@ pipeline { agent any environment { - GIT_PROJECT = "clsi-sharelatex" + GIT_PROJECT = "clsi" JENKINS_WORKFLOW = "clsi-sharelatex" TARGET_URL = "${env.JENKINS_URL}blue/organizations/jenkins/${JENKINS_WORKFLOW}/detail/$BRANCH_NAME/$BUILD_NUMBER/pipeline" - GIT_API_URL = "https://api.github.com/repos/sharelatex/${GIT_PROJECT}/statuses/$GIT_COMMIT" + GIT_API_URL = "https://api.github.com/repos/overleaf/${GIT_PROJECT}/statuses/$GIT_COMMIT" } triggers { From 663ec88718e1f41245921f0905f4badb2e1a19cd Mon Sep 17 00:00:00 2001 From: Michael Mazour <michaelmazour@gmail.com> Date: Fri, 10 May 2019 16:51:40 +0100 Subject: [PATCH 449/709] Add flags option to request JSON Adds a `flags` parameter to the request JSON, appearing under the `compile.options` key (alongside such stalwarts as `compiler`, `timeout`, etc.). This is primarily to support `-file-line-error` as an option, but could have other uses as well. `flags` should be an array of strings, or absent. If supplied, the listed arguments are added to the base latexmk command. --- app/coffee/CompileManager.coffee | 3 +- app/coffee/LatexRunner.coffee | 41 +++++++++++---------- app/coffee/RequestParser.coffee | 8 +++- test/unit/coffee/CompileManagerTests.coffee | 18 +++++---- test/unit/coffee/LatexRunnerTests.coffee | 18 +++++++++ test/unit/coffee/RequestParserTests.coffee | 32 ++++++++++++---- 6 files changed, 82 insertions(+), 38 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 0a98e87d..ad924e2b 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -93,6 +93,7 @@ module.exports = CompileManager = compiler: request.compiler timeout: request.timeout image: request.imageName + flags: request.flags environment: env }, (error, output, stats, timings) -> # request was for validation only @@ -130,7 +131,7 @@ module.exports = CompileManager = return callback(error) if error? 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 diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.coffee index 3571af20..29433f83 100644 --- a/app/coffee/LatexRunner.coffee +++ b/app/coffee/LatexRunner.coffee @@ -8,27 +8,27 @@ 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, environment} = options + {directory, mainFile, compiler, timeout, image, environment, flags} = options compiler ||= "pdflatex" timeout ||= 60000 # milliseconds - logger.log directory: directory, compiler: compiler, timeout: timeout, mainFile: mainFile, environment: environment, "starting compile" + logger.log directory: directory, compiler: compiler, timeout: timeout, mainFile: mainFile, environment: environment, flags:flags, "starting compile" # We want to run latexmk on the tex file which we will automatically # generate from the Rtex/Rmd/md file. mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, ".tex") if compiler == "pdflatex" - command = LatexRunner._pdflatexCommand mainFile + command = LatexRunner._pdflatexCommand mainFile, flags else if compiler == "latex" - command = LatexRunner._latexCommand mainFile + command = LatexRunner._latexCommand mainFile, flags else if compiler == "xelatex" - command = LatexRunner._xelatexCommand mainFile + command = LatexRunner._xelatexCommand mainFile, flags else if compiler == "lualatex" - command = LatexRunner._lualatexCommand mainFile + command = LatexRunner._lualatexCommand mainFile, flags else return callback new Error("unknown compiler: #{compiler}") - + if Settings.clsi?.strace command = ["strace", "-o", "strace", "-ff"].concat(command) @@ -63,31 +63,32 @@ module.exports = LatexRunner = else CommandRunner.kill ProcessTable[id], callback - _latexmkBaseCommand: (Settings?.clsi?.latexmkCommandPrefix || []).concat([ - "latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR", - "-synctex=1","-interaction=batchmode" - ]) + _latexmkBaseCommand: (flags) -> + args = ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR", "-synctex=1","-interaction=batchmode"] + if flags + args = args.concat(flags) + (Settings?.clsi?.latexmkCommandPrefix || []).concat(args) - _pdflatexCommand: (mainFile) -> - LatexRunner._latexmkBaseCommand.concat [ + _pdflatexCommand: (mainFile, flags) -> + LatexRunner._latexmkBaseCommand(flags).concat [ "-pdf", Path.join("$COMPILE_DIR", mainFile) ] - - _latexCommand: (mainFile) -> - LatexRunner._latexmkBaseCommand.concat [ + + _latexCommand: (mainFile, flags) -> + LatexRunner._latexmkBaseCommand(flags).concat [ "-pdfdvi", Path.join("$COMPILE_DIR", mainFile) ] - _xelatexCommand: (mainFile) -> - LatexRunner._latexmkBaseCommand.concat [ + _xelatexCommand: (mainFile, flags) -> + LatexRunner._latexmkBaseCommand(flags).concat [ "-xelatex", Path.join("$COMPILE_DIR", mainFile) ] - _lualatexCommand: (mainFile) -> - LatexRunner._latexmkBaseCommand.concat [ + _lualatexCommand: (mainFile, flags) -> + LatexRunner._latexmkBaseCommand(flags).concat [ "-lualatex", Path.join("$COMPILE_DIR", mainFile) ] diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index b887ed30..c1912f62 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -12,7 +12,7 @@ module.exports = RequestParser = compile = body.compile compile.options ||= {} - + try response.compiler = @_parseAttribute "compiler", compile.options.compiler, @@ -33,6 +33,10 @@ module.exports = RequestParser = response.check = @_parseAttribute "check", compile.options.check, type: "string" + response.flags = @_parseAttribute "flags", + compile.options.flags, + default: [], + type: "object" # The syncType specifies whether the request contains all # resources (full) or only those resources to be updated @@ -68,7 +72,7 @@ module.exports = RequestParser = originalRootResourcePath = rootResourcePath sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath) response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath) - + for resource in response.resources if resource.path == originalRootResourcePath resource.path = sanitizedRootResourcePath diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.coffee index 608a3e55..c4b0f859 100644 --- a/test/unit/coffee/CompileManagerTests.coffee +++ b/test/unit/coffee/CompileManagerTests.coffee @@ -13,7 +13,7 @@ describe "CompileManager", -> "./ResourceWriter": @ResourceWriter = {} "./OutputFileFinder": @OutputFileFinder = {} "./OutputCacheManager": @OutputCacheManager = {} - "settings-sharelatex": @Settings = + "settings-sharelatex": @Settings = path: compilesDir: "/compiles/dir" synctexBaseDir: -> "/compile" @@ -108,6 +108,7 @@ describe "CompileManager", -> compiler: @compiler = "pdflatex" timeout: @timeout = 42000 imageName: @image = "example.com/image" + flags: @flags = ["-file-line-error"] @env = {} @Settings.compileDir = "compiles" @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @@ -117,7 +118,7 @@ describe "CompileManager", -> @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) @DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) @TikzManager.checkMainFile = sinon.stub().callsArg(3, false) - + describe "normally", -> beforeEach -> @CompileManager.doCompile @request, @callback @@ -135,6 +136,7 @@ describe "CompileManager", -> compiler: @compiler timeout: @timeout image: @image + flags: @flags environment: @env }) .should.equal true @@ -146,15 +148,15 @@ describe "CompileManager", -> it "should return the output files", -> @callback.calledWith(null, @build_files).should.equal true - + it "should not inject draft mode by default", -> @DraftModeManager.injectDraftMode.called.should.equal false - + describe "with draft mode", -> beforeEach -> @request.draft = true @CompileManager.doCompile @request, @callback - + it "should inject the draft mode header", -> @DraftModeManager.injectDraftMode .calledWith(@compileDir + "/" + @rootResourcePath) @@ -173,6 +175,7 @@ describe "CompileManager", -> compiler: @compiler timeout: @timeout image: @image + flags: @flags environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1, 'CHKTEX_ULIMIT_OPTIONS': '-t 5 -v 64000'} }) .should.equal true @@ -191,6 +194,7 @@ describe "CompileManager", -> compiler: @compiler timeout: @timeout image: @image + flags: @flags environment: @env }) .should.equal true @@ -297,7 +301,7 @@ describe "CompileManager", -> @CommandRunner.run .calledWith( "#{@project_id}-#{@user_id}", - ['/opt/synctex', "pdf", synctex_path, @page, @h, @v], + ['/opt/synctex', "pdf", synctex_path, @page, @h, @v], "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}", @Settings.clsi.docker.image, 60000, @@ -330,7 +334,7 @@ describe "CompileManager", -> @directory = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" @file_path = "$COMPILE_DIR/#{@file_name}" @command =[ "texcount", "-nocol", "-inc", @file_path, "-out=" + @file_path + ".wc"] - + @CommandRunner.run .calledWith("#{@project_id}-#{@user_id}", @command, @directory, @image, @timeout, {}) .should.equal true diff --git a/test/unit/coffee/LatexRunnerTests.coffee b/test/unit/coffee/LatexRunnerTests.coffee index c26fa642..77c6edbc 100644 --- a/test/unit/coffee/LatexRunnerTests.coffee +++ b/test/unit/coffee/LatexRunnerTests.coffee @@ -59,3 +59,21 @@ describe "LatexRunner", -> mainFile = command.slice(-1)[0] mainFile.should.equal "$COMPILE_DIR/main-file.tex" + describe "with a flags option", -> + beforeEach -> + @LatexRunner.runLatex @project_id, + directory: @directory + mainFile: @mainFile + compiler: @compiler + image: @image + timeout: @timeout = 42000 + flags: ["-file-line-error", "-halt-on-error"] + @callback + + it "should include the flags in the command", -> + command = @CommandRunner.run.args[0][1] + flags = command.filter (arg) -> + (arg == "-file-line-error") || (arg == "-halt-on-error") + flags.length.should.equal 2 + flags[0].should.equal "-file-line-error" + flags[1].should.equal "-halt-on-error" diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index f63bc55e..e263e492 100644 --- a/test/unit/coffee/RequestParserTests.coffee +++ b/test/unit/coffee/RequestParserTests.coffee @@ -1,6 +1,7 @@ SandboxedModule = require('sandboxed-module') sinon = require('sinon') require('chai').should() +expect = require('chai').expect modulePath = require('path').join __dirname, '../../../app/js/RequestParser' tk = require("timekeeper") @@ -22,7 +23,7 @@ describe "RequestParser", -> resources: [] @RequestParser = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings = {} - + afterEach -> tk.reset() @@ -55,7 +56,7 @@ describe "RequestParser", -> beforeEach -> delete @validRequest.compile.options.compiler @RequestParser.parse @validRequest, (error, @data) => - + it "should set the compiler to pdflatex by default", -> @data.compiler.should.equal "pdflatex" @@ -66,6 +67,21 @@ describe "RequestParser", -> it "should set the imageName", -> @data.imageName.should.equal "basicImageName/here:2017-1" + describe "with flags set", -> + beforeEach -> + @validRequest.compile.options.flags = ["-file-line-error"] + @RequestParser.parse @validRequest, (error, @data) => + + it "should set the flags attribute", -> + expect(@data.flags).to.deep.equal ["-file-line-error"] + + describe "with flags not specified", -> + beforeEach -> + @RequestParser.parse @validRequest, (error, @data) => + + it "it should have an empty flags list", -> + expect(@data.flags).to.deep.equal [] + describe "without a timeout specified", -> beforeEach -> delete @validRequest.compile.options.timeout @@ -88,7 +104,7 @@ describe "RequestParser", -> it "should set the timeout (in milliseconds)", -> @data.timeout.should.equal @validRequest.compile.options.timeout * 1000 - + describe "with a resource without a path", -> beforeEach -> delete @validResource.path @@ -175,7 +191,7 @@ describe "RequestParser", -> it "should return the url in the parsed response", -> @data.resources[0].url.should.equal @url - + describe "with a resource with a content attribute", -> beforeEach -> @validResource.content = @content = "Hello world" @@ -185,7 +201,7 @@ describe "RequestParser", -> it "should return the content in the parsed response", -> @data.resources[0].content.should.equal @content - + describe "without a root resource path", -> beforeEach -> delete @validRequest.compile.rootResourcePath @@ -225,13 +241,13 @@ describe "RequestParser", -> } @RequestParser.parse @validRequest, @callback @data = @callback.args[0][1] - + it "should return the escaped resource", -> @data.rootResourcePath.should.equal @goodPath - + it "should also escape the resource path", -> @data.resources[0].path.should.equal @goodPath - + describe "with a root resource path that has a relative path", -> beforeEach -> @validRequest.compile.rootResourcePath = "foo/../../bar.tex" From 4f6ef616263c3277a37d53f5e36d0b1651a041f6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Thu, 6 Jun 2019 16:39:16 +0100 Subject: [PATCH 450/709] Increase the hard-timeout to 10 minutes. In practice most projects will still be limited to five minutes, but this allows us to bump up the limit for some projects, especially legacy v1 projects that have been imported to v2 --- app.coffee | 2 +- app/coffee/RequestParser.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.coffee b/app.coffee index fcf67c3c..9bcdfebc 100644 --- a/app.coffee +++ b/app.coffee @@ -34,7 +34,7 @@ app.use Metrics.http.monitor(logger) # Compile requests can take longer than the default two # minutes (including file download time), so bump up the # timeout a bit. -TIMEOUT = 6 * 60 * 1000 +TIMEOUT = 10 * 60 * 1000 app.use (req, res, next) -> req.setTimeout TIMEOUT res.setTimeout TIMEOUT diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index c1912f62..9b947124 100644 --- a/app/coffee/RequestParser.coffee +++ b/app/coffee/RequestParser.coffee @@ -2,7 +2,7 @@ settings = require("settings-sharelatex") module.exports = RequestParser = VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"] - MAX_TIMEOUT: 300 + MAX_TIMEOUT: 600 parse: (body, callback = (error, data) ->) -> response = {} From dc6af8799f29894aa4fa6f74290366eab2331fa5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane@kilkelly.me> Date: Tue, 18 Jun 2019 16:29:20 +0100 Subject: [PATCH 451/709] update logger and metrics --- Makefile | 2 +- buildscript.txt | 2 +- docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- npm-shrinkwrap.json | 573 +++++++++++++++++++++++------------------- package.json | 4 +- 6 files changed, 318 insertions(+), 267 deletions(-) diff --git a/Makefile b/Makefile index ad832318..ceb2c509 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.20 +# Version: 1.1.21 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/buildscript.txt b/buildscript.txt index 085d8cb8..9ba22b20 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -5,4 +5,4 @@ clsi --dependencies=mongo,redis --docker-repos=gcr.io/overleaf-ops --build-target=docker ---script-version=1.1.20 +--script-version=1.1.21 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index c6c35dba..39b6f427 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.20 +# Version: 1.1.21 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index 3af0f7a8..2691b89e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.20 +# Version: 1.1.21 version: "2" diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 5a2c4dc9..a221d4c6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -5,56 +5,60 @@ "requires": true, "dependencies": { "@google-cloud/common": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.27.0.tgz", - "integrity": "sha1-MdvVLXRy8mBt8FsZSIfbK1lb2tU=", - "requires": { - "@google-cloud/projectify": "^0.3.2", - "@google-cloud/promisify": "^0.3.0", - "@types/duplexify": "^3.5.0", - "@types/request": "^2.47.0", - "arrify": "^1.0.1", + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz", + "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==", + "requires": { + "@google-cloud/projectify": "^0.3.3", + "@google-cloud/promisify": "^0.4.0", + "@types/request": "^2.48.1", + "arrify": "^2.0.0", "duplexify": "^3.6.0", "ent": "^2.2.0", - "extend": "^3.0.1", - "google-auth-library": "^2.0.0", - "pify": "^4.0.0", - "retry-request": "^4.0.0" + "extend": "^3.0.2", + "google-auth-library": "^3.1.1", + "pify": "^4.0.1", + "retry-request": "^4.0.0", + "teeny-request": "^3.11.3" } }, "@google-cloud/debug-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-3.0.1.tgz", - "integrity": "sha1-jGNJQkujDbEqTVaE7v19OvpmBQ4=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-3.2.0.tgz", + "integrity": "sha512-fP87kYbS6aeDna08BivwQ1J260mwJGchRi99XdWCgqbRwuFac8ul0OT5i2wEeDSc5QaDX8ZuWQQ0igZvh1rTyQ==", "requires": { - "@google-cloud/common": "^0.27.0", - "@sindresorhus/is": "^0.13.0", - "acorn": "^5.0.3", + "@google-cloud/common": "^0.32.0", + "@sindresorhus/is": "^0.15.0", + "acorn": "^6.0.0", "coffeescript": "^2.0.0", "console-log-level": "^1.4.0", "extend": "^3.0.1", "findit2": "^2.2.3", - "gcp-metadata": "^0.9.0", + "gcp-metadata": "^1.0.0", "lodash.pickby": "^4.6.0", - "p-limit": "^2.0.0", + "p-limit": "^2.2.0", "pify": "^4.0.1", - "semver": "^5.5.0", + "semver": "^6.0.0", "source-map": "^0.6.1", - "split": "^1.0.0", - "teeny-request": "^3.11.1" + "split": "^1.0.0" }, "dependencies": { "coffeescript": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.3.2.tgz", - "integrity": "sha1-6FSnAg3+R7fPTdQSBC4y7x4mmBA=" + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.4.1.tgz", + "integrity": "sha512-34GV1aHrsMpTaO3KfMJL40ZNuvKDR/g98THHnE9bQj8HjMaZvSrLik99WWqyMhRtbe8V5hpx5iLgdcSvM/S2wg==" + }, + "semver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", + "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==" } } }, "@google-cloud/profiler": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-0.2.3.tgz", - "integrity": "sha1-Fj3738Mwuug1X+RuHlvgZTV7H1w=", + "integrity": "sha512-rNvtrFtIebIxZEJ/O0t8n7HciZGIXBo8DvHxWqAmsCaeLvkTtsaL6HmPkwxrNQ1IhbYWAxF+E/DwCiHyhKmgTg==", "requires": { "@google-cloud/common": "^0.26.0", "@types/console-log-level": "^1.4.0", @@ -76,7 +80,7 @@ "@google-cloud/common": { "version": "0.26.2", "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.26.2.tgz", - "integrity": "sha1-nFTiRxqEqgMelaJIJJduCA8lVkU=", + "integrity": "sha512-xJ2M/q3MrUbnYZuFlpF01caAlEhAUoRn0NXp93Hn3pkFpfSOG8YfbKbpBAHvcKVbBOAKVIwPsleNtuyuabUwLQ==", "requires": { "@google-cloud/projectify": "^0.3.2", "@google-cloud/promisify": "^0.3.0", @@ -92,70 +96,100 @@ "through2": "^3.0.0" } }, + "@google-cloud/promisify": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz", + "integrity": "sha512-QzB0/IMvB0eFxFK7Eqh+bfC8NLv3E9ScjWQrPOk6GgfNroxcVITdTlT8NRsRrcp5+QQJVPLkRqKG0PUdaWXmHw==" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "gcp-metadata": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz", + "integrity": "sha512-caV4S84xAjENtpezLCT/GILEAF5h/bC4cNqZFmt/tjTn8t+JBtTkQrgBrJu3857YdsnlM8rxX/PMcKGtE8hUlw==", + "requires": { + "gaxios": "^1.0.2", + "json-bigint": "^0.3.0" + } + }, + "google-auth-library": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-2.0.2.tgz", + "integrity": "sha512-FURxmo1hBVmcfLauuMRKOPYAPKht3dGuI2wjeJFalDUThO0HoYVjr4yxt5cgYSFm1dgUpmN9G/poa7ceTFAIiA==", + "requires": { + "axios": "^0.18.0", + "gcp-metadata": "^0.7.0", + "gtoken": "^2.3.0", + "https-proxy-agent": "^2.2.1", + "jws": "^3.1.5", + "lru-cache": "^5.0.0", + "semver": "^5.5.0" + }, + "dependencies": { + "gcp-metadata": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz", + "integrity": "sha512-ffjC09amcDWjh3VZdkDngIo7WoluyC5Ag9PAYxZbmQLOLNI8lvPtoKTSCyU54j2gwy5roZh6sSMTfkY2ct7K3g==", + "requires": { + "axios": "^0.18.0", + "extend": "^3.0.1", + "retry-axios": "0.3.2" + } + } + } + }, "through2": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz", - "integrity": "sha1-RotGHfnNn8wXDyLr9oUuRn5Xj/I=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", "requires": { - "readable-stream": "2 || 3", - "xtend": "~4.0.1" + "readable-stream": "2 || 3" } } } }, "@google-cloud/projectify": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.2.tgz", - "integrity": "sha1-7VTJjK5kbcA6dC6sKIGEoT0zpMI=" + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", + "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" }, "@google-cloud/promisify": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz", - "integrity": "sha1-9kHm2USo4KBe4MsQkd+mAIm+zbo=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz", + "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==" }, "@google-cloud/trace-agent": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-3.5.0.tgz", - "integrity": "sha1-GxU16eW26wAIXbbIWptLZJTjehY=", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-3.6.1.tgz", + "integrity": "sha512-KDo85aPN4gSxJ7oEIOlKd7aGENZFXAM1kbIn1Ds+61gh/K1CQWSyepgJo3nUpAwH6D1ezDWV7Iaf8ueoITc8Uw==", "requires": { - "@google-cloud/common": "^0.28.0", + "@google-cloud/common": "^0.32.1", "builtin-modules": "^3.0.0", "console-log-level": "^1.4.0", "continuation-local-storage": "^3.2.1", "extend": "^3.0.0", - "gcp-metadata": "^0.9.0", + "gcp-metadata": "^1.0.0", "hex2dec": "^1.0.1", "is": "^3.2.0", "methods": "^1.1.1", - "require-in-the-middle": "^3.0.0", - "semver": "^5.4.1", + "require-in-the-middle": "^4.0.0", + "semver": "^6.0.0", "shimmer": "^1.2.0", - "teeny-request": "^3.11.1", "uuid": "^3.0.1" }, "dependencies": { - "@google-cloud/common": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.28.0.tgz", - "integrity": "sha1-cOnTDHkOsPH/tuUpunI+ysO3prw=", - "requires": { - "@google-cloud/projectify": "^0.3.2", - "@google-cloud/promisify": "^0.3.0", - "@types/duplexify": "^3.5.0", - "@types/request": "^2.47.0", - "arrify": "^1.0.1", - "duplexify": "^3.6.0", - "ent": "^2.2.0", - "extend": "^3.0.1", - "google-auth-library": "^2.0.0", - "pify": "^4.0.0", - "retry-request": "^4.0.0" - } + "semver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", + "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==" }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -167,12 +201,12 @@ "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha1-TIVzDlm5ofHzSQR9vyQpYDS7JzU=" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha1-fvN/DQEPsCitGtWXIuUG2SYoFcs=" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "@protobufjs/eventemitter": { "version": "1.1.0", @@ -214,27 +248,24 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@sindresorhus/is": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.13.0.tgz", - "integrity": "sha1-qF0GwTZY0MyVhFN99KmUI0YfEUs=", - "requires": { - "symbol-observable": "^1.2.0" - } + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.15.0.tgz", + "integrity": "sha512-lu8BpxjAtRCAo5ifytTpCPCj99LF7o/2Myn+NXyNCBqvPYn7Pjd76AMmUB5l7XF1U6t0hcWrlEM5ESufW7wAeA==" }, "@types/caseless": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", - "integrity": "sha1-l5TGnIOF0BkqzEcaVA0fjg0WIYo=" + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, "@types/console-log-level": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz", - "integrity": "sha1-7/ccQa689RyLpa2LBdfVQkviuPM=" + "integrity": "sha512-x+OscEQwcx5Biair4enH7ov9W+clcqUWaZRaxn5IkT4yNWWjRr2oiYDkY/x1uXSTVZOQ2xlbFQySaQGB+VdXGQ==" }, "@types/duplexify": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha1-38grZL06IWj1vSZESvFlvwI33Ng=", + "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", "requires": { "@types/node": "*" } @@ -242,7 +273,7 @@ "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha1-7is7jqoRwJOCiZU2BrdFtzjFSx4=", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", "requires": { "@types/node": "*" } @@ -255,7 +286,7 @@ "@types/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha1-cZVR0jUtMBrIuB23Mqy2vcKNve8=" + "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" }, "@types/node": { "version": "10.12.15", @@ -265,7 +296,7 @@ "@types/request": { "version": "2.48.1", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha1-5ALWkapmcPu/8ZV7FfEnAjCrQvo=", + "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", "requires": { "@types/caseless": "*", "@types/form-data": "*", @@ -276,12 +307,12 @@ "@types/semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha1-FGwqKe59O65L8vyydGNuJkyBPEU=" + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==" }, "@types/tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha1-ghh4uBv6uXG5OiZaVh1U6mH5BZ8=" + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" }, "JSONStream": { "version": "1.3.2", @@ -297,6 +328,14 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -307,14 +346,14 @@ } }, "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha1-Z6ojG/iBKXS4UjWpZ3Hra9B+onk=" + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==" }, "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha1-2J5ZmfeXh1Z0wH2H8mD8Qeg+jKk=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", "requires": { "es6-promisify": "^5.0.0" } @@ -355,9 +394,9 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" }, "asn1": { "version": "0.2.4", @@ -386,7 +425,7 @@ "async-listener": { "version": "0.6.10", "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", - "integrity": "sha1-p8l6vlcLpgLXgic8DeYKUePhfLw=", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", "requires": { "semver": "^5.3.0", "shimmer": "^1.1.0" @@ -408,12 +447,12 @@ "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" }, "axios": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", - "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", "requires": { - "follow-redirects": "^1.3.0", - "is-buffer": "^1.1.5" + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" } }, "balanced-match": { @@ -421,6 +460,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -432,12 +476,15 @@ "bignumber.js": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha1-gMBIdZ2CaACAfEv9Uh5Q7bulel8=" + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" }, "bindings": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", - "integrity": "sha1-Ifx8bWfBhRbsWqooFbFF/3eybqU=" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } }, "bintrees": { "version": "1.0.1", @@ -520,9 +567,9 @@ "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=" }, "builtin-modules": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.0.0.tgz", - "integrity": "sha1-Hlh9RLAGYg2QKGzHqSOLvGEpyrE=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==" }, "bunyan": { "version": "0.22.3", @@ -634,9 +681,9 @@ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "console-log-level": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.0.tgz", - "integrity": "sha1-QDWBi+6jflhQoMA8jUUMpfLNEhc=" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.1.tgz", + "integrity": "sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ==" }, "content-disposition": { "version": "0.5.2", @@ -651,7 +698,7 @@ "continuation-local-storage": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", - "integrity": "sha1-EfYT906RT+mzTJKtLSj+auHbf/s=", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", "requires": { "async-listener": "^0.6.0", "emitter-listener": "^1.1.1" @@ -703,9 +750,9 @@ "integrity": "sha1-xPp8lUBKF6nD6Mp+FTcxK3NjMKw=" }, "delay": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/delay/-/delay-4.1.0.tgz", - "integrity": "sha1-R0zSiAnaQdGgSKcKHYNfR6w3fNI=" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz", + "integrity": "sha512-Lwaf3zVFDMBop1yDuFZ19F9WyGcZcGacsbdlZtWjQmM50tOcMntm1njF/Nb/Vjij3KaSvCF+sEYGKrrjObu2NA==" }, "delayed-stream": { "version": "1.0.0", @@ -807,9 +854,9 @@ "optional": true }, "duplexify": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", - "integrity": "sha1-saeinEq/1jlYXvrszoDWZrHjQSU=", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -827,9 +874,9 @@ } }, "ecdsa-sig-formatter": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", - "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "requires": { "safe-buffer": "^5.0.1" } @@ -842,7 +889,7 @@ "emitter-listener": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", - "integrity": "sha1-VrFA6PaZI3Wz18ssqxzHQy2WMug=", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", "requires": { "shimmer": "^1.2.0" } @@ -866,9 +913,9 @@ "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" }, "es6-promise": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", - "integrity": "sha1-2m0NVpLvtGHggsFIF/4kJ9j10FQ=" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "es6-promisify": { "version": "5.0.0", @@ -894,6 +941,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -958,6 +1010,16 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-text-encoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", + "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "finalhandler": { "version": "1.1.1", "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", @@ -1071,19 +1133,20 @@ } }, "gaxios": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.0.4.tgz", - "integrity": "sha512-5IqL40mfNrpgUQpzWkVZHLjDq62QHVn5+HmwI0Hf1haKPzAE6DftUxoGAf9pnEARwlK1A6tWmtjxLVl/kCzCFA==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", + "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", "requires": { + "abort-controller": "^3.0.0", "extend": "^3.0.2", "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.2.0" + "node-fetch": "^2.3.0" } }, "gcp-metadata": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz", - "integrity": "sha1-H510lfdGChRSZIHynhFZbdVj3SY=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz", + "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==", "requires": { "gaxios": "^1.0.2", "json-bigint": "^0.3.0" @@ -1116,37 +1179,27 @@ } }, "google-auth-library": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-2.0.2.tgz", - "integrity": "sha1-ejFdIDZ0Svavyth7IQ7mY4tA9Xs=", - "requires": { - "axios": "^0.18.0", - "gcp-metadata": "^0.7.0", - "gtoken": "^2.3.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz", + "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==", + "requires": { + "base64-js": "^1.3.0", + "fast-text-encoding": "^1.0.0", + "gaxios": "^1.2.1", + "gcp-metadata": "^1.0.0", + "gtoken": "^2.3.2", "https-proxy-agent": "^2.2.1", "jws": "^3.1.5", "lru-cache": "^5.0.0", "semver": "^5.5.0" - }, - "dependencies": { - "gcp-metadata": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz", - "integrity": "sha1-bDXbtSvaMqQnu5yY9UI33dG1QG8=", - "requires": { - "axios": "^0.18.0", - "extend": "^3.0.1", - "retry-axios": "0.3.2" - } - } } }, "google-p12-pem": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.3.tgz", - "integrity": "sha1-PYrMFAVzM5pbynsvaksga76m2Nc=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", + "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", "requires": { - "node-forge": "^0.7.5", + "node-forge": "^0.8.0", "pify": "^4.0.0" } }, @@ -1164,26 +1217,21 @@ "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" }, "gtoken": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.0.tgz", - "integrity": "sha512-Jc9/8mV630cZE9FC5tIlJCZNdUjwunvlwOtCz6IDlaiB4Sz68ki29a1+q97sWTnTYroiuF9B135rod9zrQdHLw==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", + "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", "requires": { - "axios": "^0.18.0", + "gaxios": "^1.0.4", "google-p12-pem": "^1.0.0", - "jws": "^3.1.4", + "jws": "^3.1.5", "mime": "^2.2.0", - "pify": "^3.0.0" + "pify": "^4.0.0" }, "dependencies": { "mime": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", - "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" } } }, @@ -1227,9 +1275,9 @@ } }, "hex2dec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.1.tgz", - "integrity": "sha512-E1nK1KCHaX9NvY3wkCMpZxj3oGokO5fgjcKUBaOgSkkvNogm8ngb8isKtzlxnLT37/JXLODVYXz9ti99Bxz8gg==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.2.tgz", + "integrity": "sha512-Yu+q/XWr2fFQ11tHxPq4p4EiNkb2y+lAacJNhAdRXVfRIcDH6gi7htWFnnlIzvqHMHoWeIsfXlNAjZInpAOJDA==" }, "http-errors": { "version": "1.6.3", @@ -1255,7 +1303,7 @@ "https-proxy-agent": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha1-UVUpcPoE1yPgTFbQQXjD+SWSu8A=", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", "requires": { "agent-base": "^4.1.0", "debug": "^3.1.0" @@ -1264,15 +1312,15 @@ "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -1324,7 +1372,7 @@ "is": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha1-Yc/23TxBk9uUo9YlggcrROVkXXk=" + "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" }, "is-bluebird": { "version": "1.0.2", @@ -1332,9 +1380,9 @@ "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" }, "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -1441,21 +1489,21 @@ } }, "jwa": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", - "integrity": "sha1-hyQOdsmAjb3hh4PPImTvSSnuUOY=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "requires": { "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.10", + "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "jws": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", - "integrity": "sha1-gNEtBbKT0ehB58uLTmnlYa3Pg08=", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "requires": { - "jwa": "^1.1.5", + "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, @@ -1478,37 +1526,33 @@ "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=" }, "logger-sharelatex": { - "version": "git+https://github.com/sharelatex/logger-sharelatex.git#8359f16a1546c481a5d1cde63de5b5b6bb569789", - "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.6.0", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.7.0.tgz", + "integrity": "sha512-9sxDGPSphOMDqUqGpOu/KxFAVcpydKggWv60g9D7++FDCxGkhLLn0kmBkDdgB00d1PadgX1CBMWKzIBpptDU/Q==", "requires": { - "bunyan": "1.5.1", - "coffee-script": "1.12.4", - "raven": "^1.1.3", - "request": "^2.88.0" + "bunyan": "1.8.12", + "raven": "1.1.3", + "request": "2.88.0" }, "dependencies": { "bunyan": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", - "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", + "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", "requires": { - "dtrace-provider": "~0.6", + "dtrace-provider": "~0.8", + "moment": "^2.10.6", "mv": "~2", "safe-json-stringify": "~1" } }, - "coffee-script": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.4.tgz", - "integrity": "sha1-/hvO2X/h+zknuZjytFYW4GWL4f8=" - }, "dtrace-provider": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", - "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.7.tgz", + "integrity": "sha1-3JObTT4GIM/gwc2APQ0tftBP/QQ=", "optional": true, "requires": { - "nan": "^2.0.8" + "nan": "^2.10.0" } } } @@ -1516,12 +1560,12 @@ "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha1-mntxz7fTYaGU6lVSQckvdGjVvyg=" + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha1-HaJ+ZxAnGUdpXa9oSOhH8B2EuSA=", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "requires": { "yallist": "^3.0.2" } @@ -1561,8 +1605,9 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "metrics-sharelatex": { - "version": "git+https://github.com/sharelatex/metrics-sharelatex.git#3ac1621ef049e2f2d88a83b3a41011333d609662", - "from": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.12", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.2.0.tgz", + "integrity": "sha512-kjj3EdkrOJrENLFW/QHiPqBr5AbGEHeti90nMbw6sjKO2TOcuPJHT2Y66m8tqgotnMPKw+kXToRs8Rc9+0xuMQ==", "requires": { "@google-cloud/debug-agent": "^3.0.0", "@google-cloud/profiler": "^0.2.3", @@ -1573,11 +1618,6 @@ "underscore": "~1.6.0" }, "dependencies": { - "coffee-script": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" - }, "lynx": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", @@ -1854,14 +1894,14 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, "node-fetch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", - "integrity": "sha1-Gh2UC7+5FqHT4CGfA36J5x+MX6U=" + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha1-/fO0GK7h+U8O9kLNY0hsd8qXJKw=" + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.4.tgz", + "integrity": "sha512-UOfdpxivIYY4g5tqp5FNRNgROVNxRACUxxJREntJLFaJr1E0UEqFtUIk0F/jYx/E+Y6sVXd0KDi/m5My0yGCVw==" }, "node-pre-gyp": { "version": "0.11.0", @@ -1975,17 +2015,17 @@ } }, "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "requires": { "p-try": "^2.0.0" } }, "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha1-hQgLuHxkaI+keZb+j3376CEXYLE=" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "parse-duration": { "version": "0.1.1", @@ -1993,9 +2033,9 @@ "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=" }, "parse-ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.0.0.tgz", - "integrity": "sha1-ezZAKVEAyvP6AQDMzrVmNbYvnWI=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" }, "parseurl": { "version": "1.3.2", @@ -2010,7 +2050,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", @@ -2025,12 +2065,12 @@ "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha1-SyzSXFDVmHNcUCkiJP2MbfQeMjE=" + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" }, "pretty-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz", - "integrity": "sha1-Mbr0G5T9AiJwmKqgO9YmCOsNbpI=", + "integrity": "sha512-qG66ahoLCwpLXD09ZPHSCbUWYTqdosB7SMP4OffgTgL2PBKXMuUsrk5Bwg8q4qPkjTXsKBMr+YK3Ltd/6F9s/Q==", "requires": { "parse-ms": "^2.0.0" } @@ -2041,9 +2081,9 @@ "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" }, "prom-client": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.2.0.tgz", - "integrity": "sha512-4gUAq/GR5C8q5eWxOa7tA60AtmkMpbyBd/2btCayvd3h/7HzS0p/kESKRwggJgbFrfdhTCBpOwPAwKiI01Q0VQ==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.1.tgz", + "integrity": "sha512-AcFuxVgzoA/4nlpeg9SkM2HkDjNU3V7g2LCLwpudXSbcSLiFpRMVfsCoCY5RYeR/d9jkQng1mCmVKj1mPHvP0Q==", "requires": { "tdigest": "^0.1.1" } @@ -2051,7 +2091,7 @@ "protobufjs": { "version": "6.8.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha1-yLTxKC/XqQ5vWxCe0RyEr4KQjnw=", + "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -2107,9 +2147,9 @@ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, "raven": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz", - "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/raven/-/raven-1.1.3.tgz", + "integrity": "sha1-QnPBrm005CMPUbLAEEGjK5Iygio=", "requires": { "cookie": "0.3.1", "json-stringify-safe": "5.0.1", @@ -2201,12 +2241,28 @@ "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" }, "require-in-the-middle": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-3.1.0.tgz", - "integrity": "sha1-+vS5R8jY0liK5PJGSH4tcmKt0aQ=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.0.tgz", + "integrity": "sha512-GX12iFhCUzzNuIqvei0dTLUbBEjZ420KTY/MmDxe2GQKPDGyH/wgfGMWFABpnM/M6sLwC3IaSg8A95U6gIb+HQ==", "requires": { + "debug": "^4.1.1", "module-details-from-path": "^1.0.3", - "resolve": "^1.5.0" + "resolve": "^1.10.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "require-like": { @@ -2216,9 +2272,9 @@ "dev": true }, "resolve": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", - "integrity": "sha1-oUxv36j5Kn3x2ZbLcQX6dEZY6gY=", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", + "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", "requires": { "path-parse": "^1.0.6" } @@ -2235,12 +2291,12 @@ "retry-axios": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz", - "integrity": "sha1-V1fID1hbTMTEmGqi/9R6YMbTXhM=" + "integrity": "sha512-jp4YlI0qyDFfXiXGhkCOliBN1G7fRH03Nqy8YdShzGqbY5/9S2x/IR6C88ls2DFkbWuL3ASkP7QD3pVrNpPgwQ==" }, "retry-request": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.0.0.tgz", - "integrity": "sha1-XDZhZiebPhDp16oTJ0RnoFy2kpA=", + "integrity": "sha512-S4HNLaWcMP6r8E4TMH52Y7/pM8uNayOcTDDQNBwsCccL1uI+Ol2TljxRDPzaNfbhOB30+XWP5NnZkB3LiJxi1w==", "requires": { "through2": "^2.0.0" } @@ -2475,12 +2531,12 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "requires": { "through": "2" } @@ -2583,11 +2639,6 @@ "has-flag": "^2.0.0" } }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha1-wiaIrtTqs83C3+rLtWFmBWCgCAQ=" - }, "tar": { "version": "4.4.8", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", @@ -2658,7 +2709,7 @@ "teeny-request": { "version": "3.11.3", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", - "integrity": "sha1-M1xin3ZF5dZZk2LfLzIwxMvCOlU=", + "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", "requires": { "https-proxy-agent": "^2.2.1", "node-fetch": "^2.2.0", @@ -2668,7 +2719,7 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -2697,7 +2748,7 @@ "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha1-AcHjnrMdB8t9A6lqcIIyYLIxMs0=", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "requires": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" diff --git a/package.json b/package.json index 61df3979..d01de305 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "fs-extra": "^0.16.3", "heapdump": "^0.3.5", "lockfile": "^1.0.3", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.6.0", + "logger-sharelatex": "^1.7.0", "lynx": "0.0.11", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v2.0.12", + "metrics-sharelatex": "^2.2.0", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 481a49a587b1b71c97218a7803c5460d7b5ecbe7 Mon Sep 17 00:00:00 2001 From: Tailing Yuan <yuantailing@gmail.com> Date: Fri, 4 Oct 2019 22:34:45 +0800 Subject: [PATCH 452/709] fix CompileManager and LocalCommandRunner --- app/coffee/CompileManager.coffee | 2 +- app/coffee/LocalCommandRunner.coffee | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index ad924e2b..65b78aaf 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -250,7 +250,7 @@ module.exports = CompileManager = directory = getCompileDir(project_id, user_id) timeout = 60 * 1000 # increased to allow for large projects compileName = getCompileName(project_id, user_id) - CommandRunner.run compileName, command, directory, Settings.clsi.docker.image, timeout, {}, (error, output) -> + CommandRunner.run compileName, command, directory, Settings.clsi?.docker.image, timeout, {}, (error, output) -> if error? logger.err err:error, command:command, project_id:project_id, user_id:user_id, "error running synctex" return callback(error) diff --git a/app/coffee/LocalCommandRunner.coffee b/app/coffee/LocalCommandRunner.coffee index f47af001..c5ef3c69 100644 --- a/app/coffee/LocalCommandRunner.coffee +++ b/app/coffee/LocalCommandRunner.coffee @@ -5,7 +5,7 @@ logger.info "using standard command runner" module.exports = CommandRunner = run: (project_id, command, directory, image, timeout, environment, callback = (error) ->) -> - command = (arg.replace('$COMPILE_DIR', directory) for arg in command) + command = (arg.toString().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" @@ -15,7 +15,11 @@ module.exports = CommandRunner = 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, env: env + proc = spawn command[0], command.slice(1), cwd: directory, env: env + + stdout = "" + proc.stdout.on "data", (data)-> + stdout += data proc.on "error", (err)-> logger.err err:err, project_id:project_id, command: command, directory: directory, "error running command" @@ -32,7 +36,7 @@ module.exports = CommandRunner = err.code = code return callback(err) else - callback() + callback(null, {"stdout": stdout}) return proc.pid # return process id to allow job to be killed if necessary From 775306aa63caec943928f6f99e792a3eeb00e971 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Tue, 22 Oct 2019 15:30:14 -0400 Subject: [PATCH 453/709] Send output files on timeout The unconventional use of callbacks to return both an error and data after compilation created a subtle bug where the output files were dropped by the LockManager in case of an error such as a timeout. This prevented the frontend to show error logs when a timeout occurs, creating confusion among users. We now attach the output files to the error so that they reach the controller and are sent back to the web service. --- app/coffee/CompileController.coffee | 19 ++++++++++--------- app/coffee/CompileManager.coffee | 5 +++-- app/coffee/DockerRunner.coffee | 10 +++++----- test/acceptance/coffee/TimeoutTests.coffee | 5 ++++- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.coffee index 73a7fd1a..4952d845 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -26,21 +26,19 @@ module.exports = CompileController = status = "terminated" else if error?.validate status = "validation-#{error.validate}" + else if error?.timedout + status = "timedout" + logger.log err: error, project_id: request.project_id, "timeout running compile" else if error? - if error.timedout - status = "timedout" - logger.log err: error, project_id: request.project_id, "timeout running compile" - else - status = "error" - code = 500 - logger.warn err: error, project_id: request.project_id, "error running compile" - + status = "error" + code = 500 + logger.warn err: error, project_id: request.project_id, "error running compile" else status = "failure" for file in outputFiles if file.path?.match(/output\.pdf$/) status = "success" - + if status == "failure" logger.warn project_id: request.project_id, outputFiles:outputFiles, "project failed to compile successfully, no output.pdf generated" @@ -49,6 +47,9 @@ module.exports = CompileController = if file.path is "core" logger.error project_id:request.project_id, req:req, outputFiles:outputFiles, "core file found in output" + if error? + outputFiles = error.outputFiles || [] + timer.done() res.status(code or 200).send { compile: diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 65b78aaf..792beb8d 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -106,10 +106,11 @@ module.exports = CompileManager = 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 + if error?.terminated or error?.validate or error?.timedout OutputFileFinder.findOutputFiles resourceList, compileDir, (err, outputFiles) -> return callback(err) if err? - callback(error, outputFiles) # return output files so user can check logs + error.outputFiles = outputFiles # return output files so user can check logs + callback(error) return # compile completed normally return callback(error) if error? diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 3c2ed9c5..6ea929f2 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -78,7 +78,7 @@ module.exports = DockerRunner = _callback(args...) # Only call the callback once _callback = () -> - + name = options.name streamEnded = false @@ -115,7 +115,7 @@ module.exports = DockerRunner = _getContainerOptions: (command, image, volumes, timeout, environment) -> timeoutInSeconds = timeout / 1000 - + dockerVolumes = {} for hostVol, dockerVol of volumes dockerVolumes[dockerVol] = {} @@ -148,7 +148,7 @@ module.exports = DockerRunner = "Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}] "CapDrop": "ALL" "SecurityOpt": ["no-new-privileges"] - + if Settings.path?.synctexBinHostPath? options["HostConfig"]["Binds"].push("#{Settings.path.synctexBinHostPath}:/opt/synctex:ro") @@ -276,7 +276,7 @@ module.exports = DockerRunner = logger.log container_id: containerId, "timeout reached, killing container" container.kill(() ->) , timeout - + logger.log container_id: containerId, "waiting for docker container" container.wait (error, res) -> if error? @@ -355,4 +355,4 @@ module.exports = DockerRunner = , oneHour = 60 * 60 * 1000 , randomDelay -DockerRunner.startContainerMonitor() \ No newline at end of file +DockerRunner.startContainerMonitor() diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.coffee index c3acd8f9..b274dd54 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -15,7 +15,7 @@ describe "Timed out compile", -> \\documentclass{article} \\begin{document} \\def\\x{Hello!\\par\\x} - \\x + \\x \\end{document} ''' ] @@ -29,3 +29,6 @@ describe "Timed out compile", -> it "should return a timedout status", -> @body.compile.status.should.equal "timedout" + it "should return the log output file name", -> + outputFilePaths = @body.compile.outputFiles.map((x) => x.path) + outputFilePaths.should.include('output.log') From c6af21ffd6a28d7766092fe3bccd9a234096d91e Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 24 Oct 2019 16:57:08 +0100 Subject: [PATCH 454/709] Bump build script to 1.1.23 --- Makefile | 4 ++-- buildscript.txt | 3 ++- docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index ceb2c509..a8a5c90f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.21 +# Version: 1.1.23 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -35,7 +35,7 @@ test_clean: $(DOCKER_COMPOSE) down -v -t 0 test_acceptance_pre_run: - @[ ! -f test/acceptance/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run + @[ ! -f test/acceptance/js/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/js/scripts/pre-run build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ --tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ diff --git a/buildscript.txt b/buildscript.txt index 9ba22b20..acc98e3e 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -4,5 +4,6 @@ clsi --acceptance-creds=None --dependencies=mongo,redis --docker-repos=gcr.io/overleaf-ops +--env-pass-through= --build-target=docker ---script-version=1.1.21 +--script-version=1.1.23 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 39b6f427..408d88bb 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.21 +# Version: 1.1.23 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index 2691b89e..c41d7f4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.21 +# Version: 1.1.23 version: "2" From 99b95df1ad3cf1f4747d3f16c55fd17ec5d91d3f Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Thu, 24 Oct 2019 16:58:14 +0100 Subject: [PATCH 455/709] Pass through TEXLIVE_IMAGE --- buildscript.txt | 2 +- docker-compose.ci.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/buildscript.txt b/buildscript.txt index acc98e3e..c0652224 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -4,6 +4,6 @@ clsi --acceptance-creds=None --dependencies=mongo,redis --docker-repos=gcr.io/overleaf-ops ---env-pass-through= +--env-pass-through=TEXLIVE_IMAGE --build-target=docker --script-version=1.1.23 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 408d88bb..55894f78 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -26,6 +26,7 @@ services: POSTGRES_HOST: postgres MOCHA_GREP: ${MOCHA_GREP} NODE_ENV: test + TEXLIVE_IMAGE: depends_on: - mongo - redis From c6c9bb9d3aa635a657d756e6bd7847f55ccb9048 Mon Sep 17 00:00:00 2001 From: Nate Stemen <nathanielstemen@gmail.com> Date: Fri, 25 Oct 2019 11:01:37 -0400 Subject: [PATCH 456/709] add public link to contributing docs --- .github/PULL_REQUEST_TEMPLATE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ed25ee83..12bb2eeb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,7 @@ -<!-- Please review https://github.com/overleaf/write_latex/blob/master/.github/CONTRIBUTING.md for guidance on what is expected in each section. --> + +<!-- ** This is an Overleaf public repository ** --> + +<!-- Please review https://github.com/overleaf/overleaf/blob/master/CONTRIBUTING.md for guidance on what is expected of a contribution. --> ### Description From dc02e986bf11efcde16fc2ca9a59386e2c335313 Mon Sep 17 00:00:00 2001 From: Nate Stemen <nathanielstemen@gmail.com> Date: Fri, 25 Oct 2019 11:03:45 -0400 Subject: [PATCH 457/709] bump build script to 1.1.24 --- Makefile | 2 +- buildscript.txt | 3 ++- docker-compose.ci.yml | 2 +- docker-compose.yml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a8a5c90f..ef3a7940 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.23 +# Version: 1.1.24 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/buildscript.txt b/buildscript.txt index c0652224..94c14b90 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -6,4 +6,5 @@ clsi --docker-repos=gcr.io/overleaf-ops --env-pass-through=TEXLIVE_IMAGE --build-target=docker ---script-version=1.1.23 +--script-version=1.1.24 +--public-repo=True diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 55894f78..8808d691 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.23 +# Version: 1.1.24 version: "2" diff --git a/docker-compose.yml b/docker-compose.yml index c41d7f4c..964f268c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.23 +# Version: 1.1.24 version: "2" From 10e9be3cd2cfc3d5986a0d37f0514946af034546 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Mon, 28 Oct 2019 09:31:57 -0400 Subject: [PATCH 458/709] Upgrade logging and metrics modules The new versions add the ability to send logs directly to Stackdriver. --- npm-shrinkwrap.json | 943 +++++++++++++++++++++++++++++++++++++------- package.json | 4 +- 2 files changed, 809 insertions(+), 138 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index a221d4c6..ac2f706b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -5,21 +5,19 @@ "requires": true, "dependencies": { "@google-cloud/common": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz", - "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz", + "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==", "requires": { - "@google-cloud/projectify": "^0.3.3", - "@google-cloud/promisify": "^0.4.0", - "@types/request": "^2.48.1", + "@google-cloud/projectify": "^1.0.0", + "@google-cloud/promisify": "^1.0.0", "arrify": "^2.0.0", "duplexify": "^3.6.0", "ent": "^2.2.0", "extend": "^3.0.2", - "google-auth-library": "^3.1.1", - "pify": "^4.0.1", + "google-auth-library": "^5.5.0", "retry-request": "^4.0.0", - "teeny-request": "^3.11.3" + "teeny-request": "^5.2.1" } }, "@google-cloud/debug-agent": { @@ -43,18 +41,196 @@ "split": "^1.0.0" }, "dependencies": { + "@google-cloud/common": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz", + "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==", + "requires": { + "@google-cloud/projectify": "^0.3.3", + "@google-cloud/promisify": "^0.4.0", + "@types/request": "^2.48.1", + "arrify": "^2.0.0", + "duplexify": "^3.6.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^3.1.1", + "pify": "^4.0.1", + "retry-request": "^4.0.0", + "teeny-request": "^3.11.3" + } + }, + "@google-cloud/projectify": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", + "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" + }, + "@google-cloud/promisify": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz", + "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==" + }, "coffeescript": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.4.1.tgz", "integrity": "sha512-34GV1aHrsMpTaO3KfMJL40ZNuvKDR/g98THHnE9bQj8HjMaZvSrLik99WWqyMhRtbe8V5hpx5iLgdcSvM/S2wg==" }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "gaxios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", + "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz", + "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==", + "requires": { + "gaxios": "^1.0.2", + "json-bigint": "^0.3.0" + } + }, + "google-auth-library": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz", + "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==", + "requires": { + "base64-js": "^1.3.0", + "fast-text-encoding": "^1.0.0", + "gaxios": "^1.2.1", + "gcp-metadata": "^1.0.0", + "gtoken": "^2.3.2", + "https-proxy-agent": "^2.2.1", + "jws": "^3.1.5", + "lru-cache": "^5.0.0", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "google-p12-pem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", + "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", + "requires": { + "node-forge": "^0.8.0", + "pify": "^4.0.0" + } + }, + "gtoken": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", + "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", + "requires": { + "gaxios": "^1.0.4", + "google-p12-pem": "^1.0.0", + "jws": "^3.1.5", + "mime": "^2.2.0", + "pify": "^4.0.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-forge": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", + "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" + }, "semver": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", - "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "teeny-request": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "requires": { + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0", + "uuid": "^3.3.2" + } } } }, + "@google-cloud/logging": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-5.5.3.tgz", + "integrity": "sha512-TZ/DzHod4icaC7wEEBm0PHYfbhvg0CbCVzKLsdAwj11xSD/egGNOsG5optEQcbAQEPrO1B5xBXfsE0wIBBYjpQ==", + "requires": { + "@google-cloud/common": "^2.2.2", + "@google-cloud/paginator": "^2.0.0", + "@google-cloud/projectify": "^1.0.0", + "@google-cloud/promisify": "^1.0.0", + "@opencensus/propagation-stackdriver": "0.0.18", + "arrify": "^2.0.0", + "dot-prop": "^5.1.0", + "eventid": "^0.1.2", + "extend": "^3.0.2", + "gcp-metadata": "^3.1.0", + "google-gax": "^1.7.5", + "is": "^3.3.0", + "on-finished": "^2.3.0", + "protobufjs": "^6.8.8", + "pumpify": "^2.0.0", + "snakecase-keys": "^3.0.0", + "stream-events": "^1.0.4", + "through2": "^3.0.0", + "type-fest": "^0.8.0" + } + }, + "@google-cloud/logging-bunyan": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-2.0.0.tgz", + "integrity": "sha512-9W9B8GQNMlBdQSV+c0492+sMMknn4/428EdSO1xv5Hn07P32N/e4T25y4Gnl9jlrItuZHIXRwYPVSHqUyGb1Zg==", + "requires": { + "@google-cloud/logging": "^5.5.2", + "google-auth-library": "^5.0.0" + } + }, + "@google-cloud/paginator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.1.tgz", + "integrity": "sha512-HZ6UTGY/gHGNriD7OCikYWL/Eu0sTEur2qqse2w6OVsz+57se3nTkqH14JIPxtf0vlEJ8IJN5w3BdZ22pjCB8g==", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, "@google-cloud/profiler": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-0.2.3.tgz", @@ -96,6 +272,11 @@ "through2": "^3.0.0" } }, + "@google-cloud/projectify": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", + "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" + }, "@google-cloud/promisify": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz", @@ -106,6 +287,25 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "gaxios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", + "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.3.0" + } + }, "gcp-metadata": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz", @@ -141,25 +341,72 @@ } } }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "google-p12-pem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", + "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", "requires": { - "readable-stream": "2 || 3" + "node-forge": "^0.8.0", + "pify": "^4.0.0" + } + }, + "gtoken": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", + "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", + "requires": { + "gaxios": "^1.0.4", + "google-p12-pem": "^1.0.0", + "jws": "^3.1.5", + "mime": "^2.2.0", + "pify": "^4.0.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-forge": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", + "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" + }, + "teeny-request": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "requires": { + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0", + "uuid": "^3.3.2" } } } }, "@google-cloud/projectify": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", - "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.1.tgz", + "integrity": "sha512-xknDOmsMgOYHksKc1GPbwDLsdej8aRNIA17SlSZgQdyrcC0lx0OGo4VZgYfwoEU1YS8oUxF9Y+6EzDOb0eB7Xg==" }, "@google-cloud/promisify": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz", - "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz", + "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==" }, "@google-cloud/trace-agent": { "version": "3.6.1", @@ -181,18 +428,205 @@ "uuid": "^3.0.1" }, "dependencies": { + "@google-cloud/common": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz", + "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==", + "requires": { + "@google-cloud/projectify": "^0.3.3", + "@google-cloud/promisify": "^0.4.0", + "@types/request": "^2.48.1", + "arrify": "^2.0.0", + "duplexify": "^3.6.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^3.1.1", + "pify": "^4.0.1", + "retry-request": "^4.0.0", + "teeny-request": "^3.11.3" + } + }, + "@google-cloud/projectify": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", + "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" + }, + "@google-cloud/promisify": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz", + "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "gaxios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", + "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz", + "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==", + "requires": { + "gaxios": "^1.0.2", + "json-bigint": "^0.3.0" + } + }, + "google-auth-library": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz", + "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==", + "requires": { + "base64-js": "^1.3.0", + "fast-text-encoding": "^1.0.0", + "gaxios": "^1.2.1", + "gcp-metadata": "^1.0.0", + "gtoken": "^2.3.2", + "https-proxy-agent": "^2.2.1", + "jws": "^3.1.5", + "lru-cache": "^5.0.0", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "google-p12-pem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", + "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", + "requires": { + "node-forge": "^0.8.0", + "pify": "^4.0.0" + } + }, + "gtoken": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", + "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", + "requires": { + "gaxios": "^1.0.4", + "google-p12-pem": "^1.0.0", + "jws": "^3.1.5", + "mime": "^2.2.0", + "pify": "^4.0.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-forge": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", + "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" + }, "semver": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", - "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "teeny-request": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "requires": { + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0", + "uuid": "^3.3.2" + } } } }, + "@grpc/grpc-js": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.9.tgz", + "integrity": "sha512-r1nDOEEiYmAsVYBaS4DPPqdwPOXPw7YhVOnnpPdWhlNtKbYzPash6DqWTTza9gBiYMA5d2Wiq6HzrPqsRaP4yA==", + "requires": { + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@grpc/proto-loader": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.2.tgz", + "integrity": "sha512-eBKD/FPxQoY1x6QONW2nBd54QUEyzcFP9FenujmoeDPy1rutVSHki1s/wR68F6O1QfCNDx+ayBH1O2CVNMzyyw==", + "requires": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + } + }, + "@opencensus/core": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.18.tgz", + "integrity": "sha512-PgRQXLyb3bLi8Z6pQct9erYFRdnYAZNQXAEVPf6Xq6IMkZaH20wiOTNNPxEckjI31mq5utgstAbwOn4gJiPjBQ==", + "requires": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^6.0.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@opencensus/propagation-stackdriver": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.18.tgz", + "integrity": "sha512-BLwfszIGAfqN2mqGf/atfEu84cWeoLM/YuXGfXDO1iDN2k5GXz4QFyhS8sz5l63HtsYuQqFuV+Ze7ZM0NvJp2A==", + "requires": { + "@opencensus/core": "^0.0.18", + "hex2dec": "^1.0.1", + "uuid": "^3.2.1" + } + }, + "@overleaf/o-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-2.1.0.tgz", + "integrity": "sha512-Zd9sks9LrLw8ErHt/cXeWIkyxWAqNAvNGn7wIjLQJH6TTEEW835PWOhpch+hQwwWsTxWIx/JDj+IpZ3ouw925g==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -270,14 +704,6 @@ "@types/node": "*" } }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "requires": { - "@types/node": "*" - } - }, "@types/geojson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", @@ -294,14 +720,26 @@ "integrity": "sha1-IOhWUbYv2GZW5Xycm8dxqxVwvFk=" }, "@types/request": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "version": "2.48.3", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.3.tgz", + "integrity": "sha512-3Wo2jNYwqgXcIz/rrq18AdOZUQB8cQ34CXZo+LUwPJNpvRAL86+Kc2wwI8mqpz9Cr1V+enIox5v+WZhy/p3h8w==", "requires": { "@types/caseless": "*", - "@types/form-data": "*", "@types/node": "*", - "@types/tough-cookie": "*" + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } } }, "@types/semver": { @@ -346,9 +784,9 @@ } }, "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" }, "agent-base": { "version": "4.3.0", @@ -461,9 +899,9 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -719,6 +1157,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "d64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz", + "integrity": "sha1-QAKofoUMv8n52XBrYPymE6MzbpA=" + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -841,6 +1284,14 @@ "tar-fs": "~1.16.3" } }, + "dot-prop": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.1.1.tgz", + "integrity": "sha512-QCHI6Lkf+9fJMpwfAFsTvbiSh6ujoPmhCLiDvD/n4dGtLvHfhuBwPdN6z2x4YSOwwtTcLoO/LP70xELWGF/JVA==", + "requires": { + "is-obj": "^2.0.0" + } + }, "dottie": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz", @@ -946,6 +1397,15 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "eventid": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eventid/-/eventid-0.1.2.tgz", + "integrity": "sha1-CyMtPiROpbHVKJhBQOpprH7IkhU=", + "requires": { + "d64": "^1.0.0", + "uuid": "^3.0.1" + } + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -1133,22 +1593,23 @@ } }, "gaxios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", - "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz", + "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==", "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", - "https-proxy-agent": "^2.2.1", + "https-proxy-agent": "^3.0.0", + "is-stream": "^2.0.0", "node-fetch": "^2.3.0" } }, "gcp-metadata": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz", - "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.0.tgz", + "integrity": "sha512-ympv+yQ6k5QuWCuwQqnGEvFGS7MBKdcQdj1i188v3bW9QLFIchTGaBCEZxSQapT0jffdn1vdt8oJhB5VBWQO1Q==", "requires": { - "gaxios": "^1.0.2", + "gaxios": "^2.0.1", "json-bigint": "^0.3.0" } }, @@ -1179,28 +1640,53 @@ } }, "google-auth-library": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz", - "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", + "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==", "requires": { + "arrify": "^2.0.0", "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", - "gaxios": "^1.2.1", - "gcp-metadata": "^1.0.0", - "gtoken": "^2.3.2", - "https-proxy-agent": "^2.2.1", + "gaxios": "^2.1.0", + "gcp-metadata": "^3.2.0", + "gtoken": "^4.1.0", "jws": "^3.1.5", - "lru-cache": "^5.0.0", - "semver": "^5.5.0" + "lru-cache": "^5.0.0" + } + }, + "google-gax": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.7.5.tgz", + "integrity": "sha512-Tz2DFs8umzDcCBTi2W1cY4vEgAKaYRj70g6Hh/MiiZaJizrly7PgyxsIYUGi7sOpEuAbARQymYKvy5mNi8hEbg==", + "requires": { + "@grpc/grpc-js": "0.6.9", + "@grpc/proto-loader": "^0.5.1", + "abort-controller": "^3.0.0", + "duplexify": "^3.6.0", + "google-auth-library": "^5.0.0", + "is-stream-ended": "^0.1.4", + "lodash.at": "^4.6.0", + "lodash.has": "^4.5.2", + "node-fetch": "^2.6.0", + "protobufjs": "^6.8.8", + "retry-request": "^4.0.0", + "semver": "^6.0.0", + "walkdir": "^0.4.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "google-p12-pem": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", - "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz", + "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==", "requires": { - "node-forge": "^0.8.0", - "pify": "^4.0.0" + "node-forge": "^0.9.0" } }, "graceful-fs": { @@ -1217,15 +1703,14 @@ "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" }, "gtoken": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", - "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.0.tgz", + "integrity": "sha512-wqyn2gf5buzEZN4QNmmiiW2i2JkEdZnL7Z/9p44RtZqgt4077m4khRgAYNuu8cBwHWCc6MsP6eDUn/KkF6jFIw==", "requires": { - "gaxios": "^1.0.4", - "google-p12-pem": "^1.0.0", + "gaxios": "^2.0.0", + "google-p12-pem": "^2.0.0", "jws": "^3.1.5", - "mime": "^2.2.0", - "pify": "^4.0.0" + "mime": "^2.2.0" }, "dependencies": { "mime": { @@ -1290,6 +1775,25 @@ "statuses": ">= 1.4.0 < 2" } }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -1301,11 +1805,11 @@ } }, "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", "requires": { - "agent-base": "^4.1.0", + "agent-base": "^4.3.0", "debug": "^3.1.0" }, "dependencies": { @@ -1380,9 +1884,9 @@ "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" }, "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -1392,6 +1896,21 @@ "number-is-nan": "^1.0.0" } }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -1520,19 +2039,42 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" }, + "lodash.at": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", + "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" + }, "lodash.pickby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=" }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==" + }, "logger-sharelatex": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.7.0.tgz", - "integrity": "sha512-9sxDGPSphOMDqUqGpOu/KxFAVcpydKggWv60g9D7++FDCxGkhLLn0kmBkDdgB00d1PadgX1CBMWKzIBpptDU/Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.9.0.tgz", + "integrity": "sha512-yVTuha82047IiMOQLgQHCZGKkJo6I2+2KtiFKpgkIooR2yZaoTEvAeoMwBesSDSpGUpvUJ/+9UI+PmRyc+PQKQ==", "requires": { + "@google-cloud/logging-bunyan": "^2.0.0", + "@overleaf/o-error": "^2.0.0", "bunyan": "1.8.12", "raven": "1.1.3", - "request": "2.88.0" + "request": "2.88.0", + "yn": "^3.1.1" }, "dependencies": { "bunyan": { @@ -1547,13 +2089,19 @@ } }, "dtrace-provider": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.7.tgz", - "integrity": "sha1-3JObTT4GIM/gwc2APQ0tftBP/QQ=", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", "optional": true, "requires": { - "nan": "^2.10.0" + "nan": "^2.14.0" } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "optional": true } } }, @@ -1584,6 +2132,11 @@ "statsd-parser": "~0.0.4" } }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1605,9 +2158,9 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "metrics-sharelatex": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.2.0.tgz", - "integrity": "sha512-kjj3EdkrOJrENLFW/QHiPqBr5AbGEHeti90nMbw6sjKO2TOcuPJHT2Y66m8tqgotnMPKw+kXToRs8Rc9+0xuMQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.3.0.tgz", + "integrity": "sha512-qQv4UhI0Pn89WtIEUkysy4fgaCxmIw2S7+2pUB5b15Q4dzleHNplop5peTEOf8FIcURFshjPPJiLOGCJAYph7Q==", "requires": { "@google-cloud/debug-agent": "^3.0.0", "@google-cloud/profiler": "^0.2.3", @@ -1615,7 +2168,8 @@ "coffee-script": "1.6.0", "lynx": "~0.1.1", "prom-client": "^11.1.3", - "underscore": "~1.6.0" + "underscore": "~1.6.0", + "yn": "^3.1.1" }, "dependencies": { "lynx": { @@ -1899,9 +2453,9 @@ "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "node-forge": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.4.tgz", - "integrity": "sha512-UOfdpxivIYY4g5tqp5FNRNgROVNxRACUxxJREntJLFaJr1E0UEqFtUIk0F/jYx/E+Y6sVXd0KDi/m5My0yGCVw==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" }, "node-pre-gyp": { "version": "0.11.0", @@ -2015,9 +2569,9 @@ } }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", "requires": { "p-try": "^2.0.0" } @@ -2081,9 +2635,9 @@ "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" }, "prom-client": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.1.tgz", - "integrity": "sha512-AcFuxVgzoA/4nlpeg9SkM2HkDjNU3V7g2LCLwpudXSbcSLiFpRMVfsCoCY5RYeR/d9jkQng1mCmVKj1mPHvP0Q==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz", + "integrity": "sha512-iz22FmTbtkyL2vt0MdDFY+kWof+S9UB/NACxSn2aJcewtw+EERsen0urSkZ2WrHseNdydsvcxCTAnPcSMZZv4Q==", "requires": { "tdigest": "^0.1.1" } @@ -2131,6 +2685,48 @@ "once": "^1.3.1" } }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -2156,6 +2752,13 @@ "lsmod": "1.0.0", "stack-trace": "0.0.9", "uuid": "3.0.0" + }, + "dependencies": { + "uuid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", + "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + } } }, "raw-body": { @@ -2241,13 +2844,13 @@ "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" }, "require-in-the-middle": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.0.tgz", - "integrity": "sha512-GX12iFhCUzzNuIqvei0dTLUbBEjZ420KTY/MmDxe2GQKPDGyH/wgfGMWFABpnM/M6sLwC3IaSg8A95U6gIb+HQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.1.tgz", + "integrity": "sha512-EfkM2zANyGkrfIExsECMeNn/uzjvHrE9h36yLXSavmrDiH4tgDNvltAmEKnt4PNLbqKPHZz+uszW2wTKrLUX0w==", "requires": { "debug": "^4.1.1", "module-details-from-path": "^1.0.3", - "resolve": "^1.10.0" + "resolve": "^1.12.0" }, "dependencies": { "debug": { @@ -2272,9 +2875,9 @@ "dev": true }, "resolve": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", - "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", "requires": { "path-parse": "^1.0.6" } @@ -2294,11 +2897,27 @@ "integrity": "sha512-jp4YlI0qyDFfXiXGhkCOliBN1G7fRH03Nqy8YdShzGqbY5/9S2x/IR6C88ls2DFkbWuL3ASkP7QD3pVrNpPgwQ==" }, "retry-request": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.0.0.tgz", - "integrity": "sha512-S4HNLaWcMP6r8E4TMH52Y7/pM8uNayOcTDDQNBwsCccL1uI+Ol2TljxRDPzaNfbhOB30+XWP5NnZkB3LiJxi1w==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz", + "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==", "requires": { - "through2": "^2.0.0" + "debug": "^4.1.1", + "through2": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "rimraf": { @@ -2528,6 +3147,15 @@ } } }, + "snakecase-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.1.0.tgz", + "integrity": "sha512-QM038drLbhdOY5HcRQVjO1ZJ1WR7yV5D5TIBzcOB/g3f5HURHhfpYEnvOyzXet8K+MQsgeIUA7O7vn90nAX6EA==", + "requires": { + "map-obj": "^4.0.0", + "to-snake-case": "^1.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2594,6 +3222,14 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "requires": { + "stubs": "^3.0.0" + } + }, "stream-shift": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", @@ -2630,6 +3266,11 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + }, "supports-color": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", @@ -2707,20 +3348,15 @@ } }, "teeny-request": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", - "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.0.tgz", + "integrity": "sha512-sN9E3JvEBe2CFqB/jpJpw1erWD1C7MxyYCxogHFCQSyZfkHYcdf4wzVQSw7FZxbwcfnS+FP0W9BS0mp6SEOKjg==", "requires": { - "https-proxy-agent": "^2.2.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } } }, "terraformer": { @@ -2746,12 +3382,11 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "readable-stream": "2 || 3" } }, "timekeeper": { @@ -2765,6 +3400,27 @@ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=" }, + "to-no-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", + "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=" + }, + "to-snake-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz", + "integrity": "sha1-znRpE4l5RgGah+Yu366upMYIq4w=", + "requires": { + "to-space-case": "^1.0.0" + } + }, + "to-space-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", + "integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=", + "requires": { + "to-no-case": "^1.0.0" + } + }, "toposort-class": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", @@ -2805,6 +3461,11 @@ "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", "dev": true }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", @@ -2848,9 +3509,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "v8-profiler-node8": { "version": "6.0.1", @@ -2908,6 +3569,11 @@ "extsprintf": "^1.2.0" } }, + "walkdir": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -2943,6 +3609,11 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha1-tLBJ4xS+VF486AIjbWzSLNkcPek=" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" } } } diff --git a/package.json b/package.json index d01de305..24984d1f 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "fs-extra": "^0.16.3", "heapdump": "^0.3.5", "lockfile": "^1.0.3", - "logger-sharelatex": "^1.7.0", + "logger-sharelatex": "^1.9.0", "lynx": "0.0.11", - "metrics-sharelatex": "^2.2.0", + "metrics-sharelatex": "^2.3.0", "mkdirp": "0.3.5", "mysql": "2.6.2", "request": "^2.21.0", From 7894269b8db9b1d085446de1e089b3923f87f6e8 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 7 Nov 2019 08:27:24 -0500 Subject: [PATCH 459/709] Show output files in subfolders This fixes a tiny regexp bug that prevents output files in subfolders from being shown in the "Other logs & files" panel. We also downgrade the corresponding log because it's very noisy and does not indicate a problem. --- app/coffee/OutputCacheManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.coffee index 23e179c4..5ef92ec6 100644 --- a/app/coffee/OutputCacheManager.coffee +++ b/app/coffee/OutputCacheManager.coffee @@ -65,7 +65,7 @@ module.exports = OutputCacheManager = async.mapSeries outputFiles, (file, cb) -> # don't send dot files as output, express doesn't serve them if OutputCacheManager._fileIsHidden(file.path) - logger.warn compileDir: compileDir, path: file.path, "ignoring dotfile in output" + logger.debug compileDir: compileDir, path: file.path, "ignoring dotfile in output" return cb() # copy other files into cache directory if valid newFile = _.clone(file) @@ -150,7 +150,7 @@ module.exports = OutputCacheManager = , callback _fileIsHidden: (path) -> - return path?.match(/^\.|\/./)? + return path?.match(/^\.|\/\./)? _checkFileIsSafe: (src, callback = (error, isSafe) ->) -> # check if we have a valid file to copy into the cache From 296f4cc2ffca3c63f53b1b80cf731c2ebb910f40 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Tue, 4 Feb 2020 10:50:38 -0500 Subject: [PATCH 460/709] Upgrade to local node:10.18.1 image --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d2bcb1ef..3499f178 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10.15.0 as app +FROM gcr.io/overleaf-ops/node:10.18.1 as app WORKDIR /app @@ -12,7 +12,7 @@ COPY . /app RUN npm run compile:all -FROM node:10.15.0 +FROM gcr.io/overleaf-ops/node:10.18.1 COPY --from=app /app /app From 186c8dcb2f05e71b82dc1bdf4aaa16e63109e734 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 6 Feb 2020 03:32:28 +0000 Subject: [PATCH 461/709] update to gcr.io/overleaf-ops/node:10.19.0 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3499f178..d9b6c691 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gcr.io/overleaf-ops/node:10.18.1 as app +FROM gcr.io/overleaf-ops/node:10.19.0 as app WORKDIR /app @@ -12,7 +12,7 @@ COPY . /app RUN npm run compile:all -FROM gcr.io/overleaf-ops/node:10.18.1 +FROM gcr.io/overleaf-ops/node:10.19.0 COPY --from=app /app /app From e897945a175e29b00fba2b1be37f3ab697087a99 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Fri, 7 Feb 2020 14:46:24 +0100 Subject: [PATCH 462/709] [misc] use node:10.19.0 as base image Also adjust the node version in the other build-script files. --- .nvmrc | 2 +- Dockerfile | 4 ++-- buildscript.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.nvmrc b/.nvmrc index f9fb144f..5b7269c0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.15.0 +10.19.0 diff --git a/Dockerfile b/Dockerfile index d9b6c691..ecca5f4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gcr.io/overleaf-ops/node:10.19.0 as app +FROM node:10.19.0 as app WORKDIR /app @@ -12,7 +12,7 @@ COPY . /app RUN npm run compile:all -FROM gcr.io/overleaf-ops/node:10.19.0 +FROM node:10.19.0 COPY --from=app /app /app diff --git a/buildscript.txt b/buildscript.txt index 94c14b90..fefbb77e 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,6 +1,6 @@ clsi --language=coffeescript ---node-version=10.15.0 +--node-version=10.19.0 --acceptance-creds=None --dependencies=mongo,redis --docker-repos=gcr.io/overleaf-ops From ec628a4e59da423bf675e76773ed9f86bb911af4 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 6 Feb 2020 14:46:30 +0000 Subject: [PATCH 463/709] support other runtimes --- app/coffee/DockerRunner.coffee | 4 +++- config/settings.defaults.coffee | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.coffee index 6ea929f2..8b49410e 100644 --- a/app/coffee/DockerRunner.coffee +++ b/app/coffee/DockerRunner.coffee @@ -149,10 +149,12 @@ module.exports = DockerRunner = "CapDrop": "ALL" "SecurityOpt": ["no-new-privileges"] - if Settings.path?.synctexBinHostPath? options["HostConfig"]["Binds"].push("#{Settings.path.synctexBinHostPath}:/opt/synctex:ro") + if Settings.clsi.docker.runtime? + options["HostConfig"]["Runtime"] = Settings.clsi.docker.runtime + if Settings.clsi.docker.seccomp_profile? options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}" diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index ad3f04d8..60075b78 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -50,6 +50,7 @@ if process.env["DOCKER_RUNNER"] module.exports.clsi = dockerRunner: process.env["DOCKER_RUNNER"] == "true" docker: + runtime: process.env["DOCKER_RUNTIME"] image: process.env["TEXLIVE_IMAGE"] or "quay.io/sharelatex/texlive-full:2017.1" env: HOME: "/tmp" From 1fc3292966403280604324346fa187db49b9b38d Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Mon, 10 Feb 2020 17:10:44 +0100 Subject: [PATCH 464/709] [misc] update the build scripts to 1.3.5 --- Dockerfile | 19 +++++++++++++------ Jenkinsfile | 1 + Makefile | 9 ++++++++- buildscript.txt | 6 +++--- docker-compose-config.yml | 2 +- docker-compose.ci.yml | 14 +++++++------- docker-compose.yml | 31 +++++++++++++------------------ 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index ecca5f4f..9dcf70c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,17 @@ -FROM node:10.19.0 as app +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/sharelatex/sharelatex-dev-environment +# Version: 1.3.5 + +FROM node:10.19.0 as base WORKDIR /app +COPY install_deps.sh /app +RUN chmod 0755 ./install_deps.sh && ./install_deps.sh +ENTRYPOINT ["/bin/sh", "entrypoint.sh"] +COPY entrypoint.sh /app + +FROM base as app #wildcard as some files may not be in all repos COPY package*.json npm-shrink*.json /app/ @@ -12,12 +23,8 @@ COPY . /app RUN npm run compile:all -FROM node:10.19.0 +FROM base COPY --from=app /app /app -WORKDIR /app -RUN chmod 0755 ./install_deps.sh && ./install_deps.sh -ENTRYPOINT ["/bin/sh", "entrypoint.sh"] - CMD ["node", "--expose-gc", "app.js"] diff --git a/Jenkinsfile b/Jenkinsfile index 82bd9736..47a66382 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -16,6 +16,7 @@ pipeline { } stages { + stage('Install') { steps { withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { diff --git a/Makefile b/Makefile index ef3a7940..13785bc2 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.24 +# Version: 1.3.5 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -28,14 +28,20 @@ test_unit: test_acceptance: test_clean test_acceptance_pre_run test_acceptance_run +test_acceptance_debug: test_clean test_acceptance_pre_run test_acceptance_run_debug + test_acceptance_run: @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance +test_acceptance_run_debug: + @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk + test_clean: $(DOCKER_COMPOSE) down -v -t 0 test_acceptance_pre_run: @[ ! -f test/acceptance/js/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/js/scripts/pre-run + build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ --tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ @@ -48,4 +54,5 @@ publish: docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/buildscript.txt b/buildscript.txt index fefbb77e..e6e202ca 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,10 +1,10 @@ clsi +--public-repo=True --language=coffeescript +--env-add= --node-version=10.19.0 --acceptance-creds=None --dependencies=mongo,redis --docker-repos=gcr.io/overleaf-ops --env-pass-through=TEXLIVE_IMAGE ---build-target=docker ---script-version=1.1.24 ---public-repo=True +--script-version=1.3.5 diff --git a/docker-compose-config.yml b/docker-compose-config.yml index c8b7dcc2..392f8fea 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -1,4 +1,4 @@ -version: "2" +version: "2.3" services: dev: diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 8808d691..d80a4ce1 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,9 +1,9 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.24 +# Version: 1.3.5 -version: "2" +version: "2.3" services: test_unit: @@ -28,12 +28,13 @@ services: NODE_ENV: test TEXLIVE_IMAGE: depends_on: - - mongo - - redis + mongo: + condition: service_healthy + redis: + condition: service_healthy command: npm run test:acceptance:_run - tar: build: . image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER @@ -41,9 +42,8 @@ services: - ./:/tmp/build/ command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . user: root - redis: image: redis mongo: - image: mongo:3.4 + image: mongo:3.6 diff --git a/docker-compose.yml b/docker-compose.yml index 964f268c..8cc59733 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,15 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.1.24 +# Version: 1.3.5 -version: "2" +version: "2.3" services: test_unit: - build: . + build: + context: . + target: base volumes: - .:/app working_dir: /app @@ -17,7 +19,9 @@ services: command: npm run test:unit test_acceptance: - build: . + build: + context: . + target: base volumes: - .:/app working_dir: /app @@ -33,24 +37,15 @@ services: LOG_LEVEL: ERROR NODE_ENV: test depends_on: - - mongo - - redis + mongo: + condition: service_healthy + redis: + condition: service_healthy command: npm run test:acceptance - - - tar: - build: . - image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER - volumes: - - ./:/tmp/build/ - command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . - user: root - redis: image: redis mongo: - image: mongo:3.4 - + image: mongo:3.6 From 82cc99c6326d901ea66e46c09dffd19888363f52 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 12 Feb 2020 12:37:00 +0000 Subject: [PATCH 465/709] remove unused .travis.yml file --- .travis.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b6fb0e91..00000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: node_js - -before_install: - - npm install -g grunt-cli - -install: - - npm install - - grunt install - -script: - - grunt test:unit - -services: - - redis-server - - mongodb From be28b9f6f9b4e206f319797b4918b8a90648a4ec Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:06:09 +0100 Subject: [PATCH 466/709] removed unneeded default function arg preventing from decaffeination --- app/coffee/DockerLockManager.coffee | 2 +- app/coffee/LockManager.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/coffee/DockerLockManager.coffee b/app/coffee/DockerLockManager.coffee index 739f2cd1..bf90f023 100644 --- a/app/coffee/DockerLockManager.coffee +++ b/app/coffee/DockerLockManager.coffee @@ -46,7 +46,7 @@ module.exports = LockManager = logger.error {key:key, lock: existingLock}, "tried to release lock that has gone" callback() - runWithLock: (key, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) -> + runWithLock: (key, runner, callback = ( (error) -> )) -> LockManager.getLock key, (error, lockValue) -> return callback(error) if error? runner (error1, args...) -> diff --git a/app/coffee/LockManager.coffee b/app/coffee/LockManager.coffee index afa3cca9..5d9fe26a 100644 --- a/app/coffee/LockManager.coffee +++ b/app/coffee/LockManager.coffee @@ -9,7 +9,7 @@ module.exports = LockManager = MAX_LOCK_WAIT_TIME: 15000 # 10s maximum time to spend trying to get the lock LOCK_STALE: 5*60*1000 # 5 mins time until lock auto expires - runWithLock: (path, runner = ((releaseLock = (error) ->) ->), callback = ((error) ->)) -> + runWithLock: (path, runner, callback = ((error) ->)) -> lockOpts = wait: @MAX_LOCK_WAIT_TIME pollPeriod: @LOCK_TEST_INTERVAL From 725074c09dd7443e42f57148ec0da4a70bf3a2b0 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:10:00 +0100 Subject: [PATCH 467/709] decaffeinate: update build scripts to es --- .dockerignore | 2 -- .eslintrc | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ .prettierrc | 8 ++++++ Dockerfile | 1 - Jenkinsfile | 7 ++++++ Makefile | 15 ++++++++---- buildscript.txt | 2 +- nodemon.json | 7 +++--- package.json | 18 ++++++-------- 9 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 .eslintrc create mode 100644 .prettierrc diff --git a/.dockerignore b/.dockerignore index 386f26df..ba1c3442 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,5 +5,3 @@ gitrev .npm .nvmrc nodemon.json -app.js -**/js/* diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..42a4b5ca --- /dev/null +++ b/.eslintrc @@ -0,0 +1,65 @@ +// this file was auto-generated, do not edit it directly. +// instead run bin/update_build_scripts from +// https://github.com/sharelatex/sharelatex-dev-environment +// Version: 1.3.5 +{ + "extends": [ + "standard", + "prettier", + "prettier/standard" + ], + "parserOptions": { + "ecmaVersion": 2017 + }, + "plugins": [ + "mocha", + "chai-expect", + "chai-friendly" + ], + "env": { + "node": true, + "mocha": true + }, + "rules": { + // Swap the no-unused-expressions rule with a more chai-friendly one + "no-unused-expressions": 0, + "chai-friendly/no-unused-expressions": "error" + }, + "overrides": [ + { + // Test specific rules + "files": ["test/**/*.js"], + "globals": { + "expect": true + }, + "rules": { + // mocha-specific rules + "mocha/handle-done-callback": "error", + "mocha/no-exclusive-tests": "error", + "mocha/no-global-tests": "error", + "mocha/no-identical-title": "error", + "mocha/no-nested-tests": "error", + "mocha/no-pending-tests": "error", + "mocha/no-skipped-tests": "error", + "mocha/no-mocha-arrows": "error", + + // chai-specific rules + "chai-expect/missing-assertion": "error", + "chai-expect/terminating-properties": "error", + + // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests. + // we don't enforce this at the top-level - just in tests to manage `this` scope + // based on mocha's context mechanism + "mocha/prefer-arrow-callback": "error" + } + }, + { + // Backend specific rules + "files": ["app/**/*.js", "app.js", "index.js"], + "rules": { + // don't allow console.log in backend code + "no-console": "error" + } + } + ] +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..5845b821 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/sharelatex/sharelatex-dev-environment +# Version: 1.3.5 +{ + "semi": false, + "singleQuote": true +} diff --git a/Dockerfile b/Dockerfile index 9dcf70c3..9faccd49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,6 @@ RUN npm install --quiet COPY . /app -RUN npm run compile:all FROM base diff --git a/Jenkinsfile b/Jenkinsfile index 47a66382..c7b961eb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -37,6 +37,13 @@ pipeline { } } + stage('Linting') { + steps { + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make format' + sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make lint' + } + } + stage('Unit Tests') { steps { sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit' diff --git a/Makefile b/Makefile index 13785bc2..88234f2a 100644 --- a/Makefile +++ b/Makefile @@ -16,12 +16,17 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ clean: docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - rm -f app.js - rm -rf app/js - rm -rf test/unit/js - rm -rf test/acceptance/js -test: test_unit test_acceptance +format: + $(DOCKER_COMPOSE) run --rm test_unit npm run format + +format_fix: + $(DOCKER_COMPOSE) run --rm test_unit npm run format:fix + +lint: + $(DOCKER_COMPOSE) run --rm test_unit npm run lint + +test: format lint test_unit test_acceptance test_unit: @[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit diff --git a/buildscript.txt b/buildscript.txt index e6e202ca..9cccf33a 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,6 +1,6 @@ clsi --public-repo=True ---language=coffeescript +--language=es --env-add= --node-version=10.19.0 --acceptance-creds=None diff --git a/nodemon.json b/nodemon.json index 98db38d7..5826281b 100644 --- a/nodemon.json +++ b/nodemon.json @@ -10,10 +10,9 @@ }, "watch": [ - "app/coffee/", - "app.coffee", + "app/js/", + "app.js", "config/" ], - "ext": "coffee" - + "ext": "js" } diff --git a/package.json b/package.json index 24984d1f..55395380 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,15 @@ "url": "https://github.com/sharelatex/clsi-sharelatex.git" }, "scripts": { - "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')", - "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js", - "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js", - "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP", - "compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee", - "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee", - "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests", + "start": "node $NODE_APP_OPTIONS app.js", + "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", - "compile:smoke_tests": "[ ! -e test/smoke/coffee ] && echo 'No smoke tests to compile' || coffee -o test/smoke/js -c test/smoke/coffee" + "lint": "node_modules/.bin/eslint .", + "format": "node_modules/.bin/prettier-eslint '**/*.js' --list-different", + "format:fix": "node_modules/.bin/prettier-eslint '**/*.js' --write" }, "author": "James Allen <james@sharelatex.com>", "dependencies": { From e14da0f9a61f9cac49ce9872922122c2a786c864 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:11:31 +0100 Subject: [PATCH 468/709] decaffeinate: update .gitignore --- .gitignore | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitignore b/.gitignore index 048a75b9..912e3801 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,7 @@ **.swp node_modules -app/js -test/unit/js -test/smoke/js -test/acceptance/js test/acceptance/fixtures/tmp compiles -app.js -**/*.map .DS_Store *~ cache From f8fff476dd21c3b375614432b61904eb0383e326 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:13:44 +0100 Subject: [PATCH 469/709] decaffeinate: add eslint and prettier packages --- npm-shrinkwrap.json | 3378 +++++++++++++++++++++++++++++++++++++++++-- package.json | 18 + 2 files changed, 3251 insertions(+), 145 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ac2f706b..40fed3c2 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4,6 +4,166 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", + "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", + "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "dev": true + }, + "@babel/runtime": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz", + "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", + "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.4", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.4", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, "@google-cloud/common": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz", @@ -704,11 +864,23 @@ "@types/node": "*" } }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/geojson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", "integrity": "sha1-PgKXJyjGkkjCrwjWCkjLuGgP/98=" }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, "@types/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", @@ -752,6 +924,59 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" }, + "@typescript-eslint/experimental-utils": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", + "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-scope": "^4.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz", + "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "1.13.0", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", + "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, "JSONStream": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", @@ -788,6 +1013,12 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "dev": true + }, "agent-base": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", @@ -807,11 +1038,29 @@ "uri-js": "^4.2.2" } }, + "ansi-escapes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", + "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -826,11 +1075,59 @@ "readable-stream": "^2.0.6" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -855,6 +1152,18 @@ "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", "dev": true }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", @@ -893,6 +1202,26 @@ "is-buffer": "^2.0.2" } }, + "axobject-query": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz", + "integrity": "sha512-ICt34ZmrVt8UQnvPl6TVyDTkmhXmAyAT4Jh5ugfGUX4MOrZ+U/ZY6/sdylRw3qGNr9Ub5AJsaHeDMzNLehRdOQ==", + "dev": true + }, + "babel-eslint": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz", + "integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -960,6 +1289,12 @@ "type-is": "~1.6.16" } }, + "boolify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/boolify/-/boolify-1.0.1.tgz", + "integrity": "sha1-tcCeF8rNET0Rt7s+04TMASmU2Gs=", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1039,6 +1374,29 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.1.2.tgz", + "integrity": "sha512-QfFrU0CIw2oltVvpndW32kuJ/9YOJwUnmWrjlXt1nnJZHCaS9i6bfOpg9R4Lw8aZjStkJWM+jc0cdXjWBgVJSw==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1054,11 +1412,111 @@ "deep-eql": "0.1.3" } }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "chownr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", "integrity": "sha1-VHJri4//TfBTxCGH6AH7RBLfFJQ=" }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "cls-bluebird": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", @@ -1084,6 +1542,21 @@ "integrity": "sha1-bdTeHrYveE2MjYCWdVLLpUf/2d4=", "dev": true }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "combined-stream": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", @@ -1097,6 +1570,12 @@ "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz", "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" }, + "common-tags": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1123,6 +1602,12 @@ "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.1.tgz", "integrity": "sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ==" }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, "content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -1152,16 +1637,41 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "core-js": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", + "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, "d64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz", "integrity": "sha1-QAKofoUMv8n52XBrYPymE6MzbpA=" }, + "damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1178,6 +1688,12 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, "deep-eql": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", @@ -1192,6 +1708,21 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha1-xPp8lUBKF6nD6Mp+FTcxK3NjMKw=" }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "delay": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz", @@ -1227,6 +1758,12 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "docker-modem": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.7.tgz", @@ -1284,6 +1821,15 @@ "tar-fs": "~1.16.3" } }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dot-prop": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.1.1.tgz", @@ -1345,6 +1891,12 @@ "shimmer": "^1.2.0" } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -1363,10 +1915,49 @@ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "es6-promisify": { "version": "5.0.0", @@ -1387,6 +1978,470 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "eslint": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", + "integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz", + "integrity": "sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-config-standard": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz", + "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==", + "dev": true + }, + "eslint-config-standard-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-8.1.0.tgz", + "integrity": "sha512-ULVC8qH8qCqbU792ZOO6DaiaZyHNS/5CZt3hKqHkEhVlhPEPN3nfBqqxJCyp59XrjIBZPu1chMYe9T2DXZ7TMw==", + "dev": true + }, + "eslint-config-standard-react": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-react/-/eslint-config-standard-react-9.2.0.tgz", + "integrity": "sha512-u+KRP2uCtthZ/W4DlLWCC59GZNV/y9k9yicWWammgTs/Omh8ZUUPF3EnYm81MAcbkYQq2Wg0oxutAhi/FQ8mIw==", + "dev": true, + "requires": { + "eslint-config-standard-jsx": "^8.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", + "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "eslint-module-utils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz", + "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + } + }, + "eslint-plugin-chai-expect": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-expect/-/eslint-plugin-chai-expect-2.1.0.tgz", + "integrity": "sha512-rd0/4mjMV6c3i0o4DKkWI4uaFN9DK707kW+/fDphaDI6HVgxXnhML9Xgt5vHnTXmSSnDhupuCFBgsEAEpchXmQ==", + "dev": true + }, + "eslint-plugin-chai-friendly": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.5.0.tgz", + "integrity": "sha512-Pxe6z8C9fP0pn2X2nGFU/b3GBOCM/5FVus1hsMwJsXP3R7RiXFl7g0ksJbsc0GxiLyidTW4mEFk77qsNn7Tk7g==", + "dev": true + }, + "eslint-plugin-es": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.0.tgz", + "integrity": "sha512-6/Jb/J/ZvSebydwbBJO1R9E5ky7YeElfK56Veh7e4QGFHCXoIXGH9HhVz+ibJLM3XJ1XjP+T7rKBLUa/Y7eIng==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "regexpp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", + "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", + "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.1", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", + "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5", + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.2", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^7.0.2", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + } + } + }, + "eslint-plugin-mocha": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.2.2.tgz", + "integrity": "sha512-oNhPzfkT6Q6CJ0HMVJ2KLxEWG97VWGTmuHOoRcDLE0U88ugUyFNV9wrT2XIt5cGtqc5W9k38m4xTN34L09KhBA==", + "dev": true, + "requires": { + "ramda": "^0.26.1" + } + }, + "eslint-plugin-node": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.0.0.tgz", + "integrity": "sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg==", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz", + "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true + }, + "eslint-plugin-react": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.3.tgz", + "integrity": "sha512-Bt56LNHAQCoou88s8ViKRjMB2+36XRejCQ1VoLj716KI1MoE99HpTVvIThJ0rvFmG4E4Gsq+UgToEjn+j044Bg==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.3", + "object.entries": "^1.1.1", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.14.2", + "string.prototype.matchall": "^4.0.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "eslint-plugin-standard": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", + "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", + "dev": true + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "dev": true, + "requires": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", + "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1455,6 +2510,28 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1465,16 +2542,46 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, "fast-text-encoding": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1501,23 +2608,60 @@ } } }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "findit2": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz", "integrity": "sha1-WKRmaX34piBc39vzlVNri9d3pfY=" }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, "requires": { - "debug": "=3.1.0" + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { "ms": "2.0.0" } @@ -1577,6 +2721,18 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -1618,6 +2774,18 @@ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz", "integrity": "sha1-kv9xllINZwg5pnMICSoSqt8valk=" }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1639,6 +2807,21 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "google-auth-library": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", @@ -1734,12 +2917,36 @@ "har-schema": "^2.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "has-flag": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -1764,6 +2971,12 @@ "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.2.tgz", "integrity": "sha512-Yu+q/XWr2fFQ11tHxPq4p4EiNkb2y+lAacJNhAdRXVfRIcDH6gi7htWFnnlIzvqHMHoWeIsfXlNAjZInpAOJDA==" }, + "hosted-git-info": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", + "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "dev": true + }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -1836,6 +3049,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, "ignore-walk": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", @@ -1844,6 +3063,28 @@ "minimatch": "^3.0.4" } }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", @@ -1868,6 +3109,97 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" }, + "inquirer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", + "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + } + } + }, + "internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + } + }, "ipaddr.js": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", @@ -1878,6 +3210,12 @@ "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, "is-bluebird": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", @@ -1888,6 +3226,24 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -1896,11 +3252,35 @@ "number-is-nan": "^1.0.0" } }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -1911,6 +3291,21 @@ "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -1921,6 +3316,12 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1947,11 +3348,33 @@ } } }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, "json-bigint": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", @@ -1970,6 +3393,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -2007,6 +3436,16 @@ "verror": "1.10.0" } }, + "jsx-ast-utils": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", + "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "object.assign": "^4.1.0" + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -2026,6 +3465,52 @@ "safe-buffer": "^5.0.1" } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "lockfile": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", @@ -2054,11 +3539,29 @@ "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "lodash.pickby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=" }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -2105,11 +3608,63 @@ } } }, + "loglevel": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", + "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", + "dev": true + }, + "loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha1-akAhj9x64V/HbD0PPmdsRlOIYD4=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2132,6 +3687,24 @@ "statsd-parser": "~0.0.4" } }, + "make-plural": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz", + "integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, "map-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", @@ -2152,6 +3725,29 @@ "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" }, + "messageformat": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz", + "integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==", + "dev": true, + "requires": { + "make-plural": "^4.3.0", + "messageformat-formatters": "^2.0.1", + "messageformat-parser": "^4.1.2" + } + }, + "messageformat-formatters": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz", + "integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==", + "dev": true + }, + "messageformat-parser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.2.tgz", + "integrity": "sha512-7dWuifeyldz7vhEuL96Kwq1fhZXBW+TUfbnHN4UCrCxoXQTYjHnR78eI66Gk9LaLLsAvzPNVJBaa66DRfFNaiA==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2206,6 +3802,12 @@ "mime-db": "~1.37.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2334,6 +3936,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -2426,6 +4034,12 @@ "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", "integrity": "sha1-pgO0pJirdxc2ErnqGs3sTZgPALs=" }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "ncp": { "version": "2.0.0", "resolved": "http://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -2447,6 +4061,12 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -2484,143 +4104,957 @@ } } }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha1-PBcyt7qTazoQMlrvYWRnwMy8yXk=" - }, - "npm-packlist": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", - "integrity": "sha1-Ir3i68EucspIKr1nr8UetJN3JDo=", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha1-PBcyt7qTazoQMlrvYWRnwMy8yXk=" + }, + "npm-packlist": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", + "integrity": "sha1-Ir3i68EucspIKr1nr8UetJN3JDo=", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.entries": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", + "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", + "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + }, + "dependencies": { + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-duration": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz", + "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=" + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "prettier-eslint": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-9.0.1.tgz", + "integrity": "sha512-KZT65QTosSAqBBqmrC+RpXbsMRe7Os2YSR9cAfFbDlyPAopzA/S5bioiZ3rpziNQNSJaOxmtXSx07EQ+o2Dlug==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^1.10.2", + "common-tags": "^1.4.0", + "core-js": "^3.1.4", + "dlv": "^1.1.0", + "eslint": "^5.0.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^1.7.0", + "pretty-format": "^23.0.1", + "require-relative": "^0.8.7", + "typescript": "^3.2.1", + "vue-eslint-parser": "^2.0.2" + }, + "dependencies": { + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "prettier-eslint-cli": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/prettier-eslint-cli/-/prettier-eslint-cli-5.0.0.tgz", + "integrity": "sha512-cei9UbN1aTrz3sQs88CWpvY/10PYTevzd76zoG1tdJ164OhmNTFRKPTOZrutVvscoQWzbnLKkviS3gu5JXwvZg==", + "dev": true, + "requires": { + "arrify": "^2.0.1", + "boolify": "^1.0.0", + "camelcase-keys": "^6.0.0", + "chalk": "^2.4.2", + "common-tags": "^1.8.0", + "core-js": "^3.1.4", + "eslint": "^5.0.0", + "find-up": "^4.1.0", + "get-stdin": "^7.0.0", + "glob": "^7.1.4", + "ignore": "^5.1.2", + "lodash.memoize": "^4.1.2", + "loglevel-colored-level-prefix": "^1.0.0", + "messageformat": "^2.2.1", + "prettier-eslint": "^9.0.0", + "rxjs": "^6.5.2", + "yargs": "^13.2.4" + }, + "dependencies": { + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "dev": true + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "fast-diff": "^1.1.2" } }, - "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, "requires": { - "p-try": "^2.0.0" + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parse-duration": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz", - "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=" - }, - "parse-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", - "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, "pretty-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz", @@ -2634,6 +5068,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "prom-client": { "version": "11.5.3", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz", @@ -2642,6 +5082,17 @@ "tdigest": "^0.1.1" } }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, "protobufjs": { "version": "6.8.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", @@ -2737,6 +5188,18 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=" }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "ramda": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", + "dev": true + }, "range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", @@ -2790,6 +5253,33 @@ } } }, + "react-is": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", + "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", @@ -2804,6 +5294,28 @@ "util-deprecate": "~1.0.1" } }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -2843,6 +5355,12 @@ "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, "require-in-the-middle": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.1.tgz", @@ -2874,12 +5392,40 @@ "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", + "dev": true + }, "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", "requires": { - "path-parse": "^1.0.6" + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" } }, "retry-as-promised": { @@ -2928,6 +5474,24 @@ "glob": "^7.0.5" } }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", + "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3071,11 +5635,36 @@ "coffee-script": "1.6.0" } }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "shimmer": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", "integrity": "sha1-+Wb3VVeJdj502IQRk2haXnhzZmU=" }, + "side-channel": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", + "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "object-inspect": "^1.7.0" + } + }, "sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", @@ -3095,6 +5684,25 @@ "buster-format": "~0.5" } }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, "smoke-test-sharelatex": { "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", "from": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", @@ -3161,6 +5769,38 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -3174,6 +5814,12 @@ "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "sqlite3": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.6.tgz", @@ -3245,6 +5891,40 @@ "strip-ansi": "^3.0.0" } }, + "string.prototype.matchall": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", + "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.2" + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3261,6 +5941,12 @@ "ansi-regex": "^2.0.0" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3280,6 +5966,82 @@ "has-flag": "^2.0.0" } }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "tar": { "version": "4.4.8", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", @@ -3376,6 +6138,12 @@ "terraformer": "~1.0.5" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -3395,11 +6163,26 @@ "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", "dev": true }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=" }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, "to-no-case": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", @@ -3442,6 +6225,12 @@ } } }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3455,6 +6244,15 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-detect": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", @@ -3480,6 +6278,12 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typescript": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "dev": true + }, "underscore": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", @@ -3513,6 +6317,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, "v8-profiler-node8": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/v8-profiler-node8/-/v8-profiler-node8-6.0.1.tgz", @@ -3549,6 +6359,16 @@ } } }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "validator": { "version": "10.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-10.9.0.tgz", @@ -3569,11 +6389,100 @@ "extsprintf": "^1.2.0" } }, + "vue-eslint-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", + "integrity": "sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.2", + "esquery": "^1.0.0", + "lodash": "^4.17.4" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "walkdir": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -3590,6 +6499,63 @@ "@types/node": "*" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3600,16 +6566,138 @@ "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha1-tLBJ4xS+VF486AIjbWzSLNkcPek=" }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 55395380..c38621aa 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,28 @@ "wrench": "~1.5.4" }, "devDependencies": { + "babel-eslint": "^10.0.3", "bunyan": "^0.22.1", "chai": "~1.8.1", "coffeescript": "1.6.0", + "eslint": "^6.6.0", + "eslint-config-prettier": "^6.10.0", + "eslint-config-standard": "^14.1.0", + "eslint-config-standard-jsx": "^8.1.0", + "eslint-config-standard-react": "^9.2.0", + "eslint-plugin-chai-expect": "^2.1.0", + "eslint-plugin-chai-friendly": "^0.5.0", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-mocha": "^6.2.2", + "eslint-plugin-node": "^11.0.0", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-react": "^7.18.3", + "eslint-plugin-standard": "^4.0.1", "mocha": "^4.0.1", + "prettier": "^1.19.1", + "prettier-eslint-cli": "^5.0.0", "sandboxed-module": "~0.3.0", "sinon": "~1.7.3", "timekeeper": "0.0.4" From 37794788ce873c399726fa8cec72a103c295d620 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:14:01 +0100 Subject: [PATCH 470/709] decaffeinate: Rename CommandRunner.coffee and 25 other files from .coffee to .js --- app/coffee/{CommandRunner.coffee => CommandRunner.js} | 0 app/coffee/{CompileController.coffee => CompileController.js} | 0 app/coffee/{CompileManager.coffee => CompileManager.js} | 0 app/coffee/{ContentTypeMapper.coffee => ContentTypeMapper.js} | 0 app/coffee/{DbQueue.coffee => DbQueue.js} | 0 app/coffee/{DockerLockManager.coffee => DockerLockManager.js} | 0 app/coffee/{DockerRunner.coffee => DockerRunner.js} | 0 app/coffee/{DraftModeManager.coffee => DraftModeManager.js} | 0 app/coffee/{Errors.coffee => Errors.js} | 0 app/coffee/{LatexRunner.coffee => LatexRunner.js} | 0 app/coffee/{LocalCommandRunner.coffee => LocalCommandRunner.js} | 0 app/coffee/{LockManager.coffee => LockManager.js} | 0 app/coffee/{Metrics.coffee => Metrics.js} | 0 app/coffee/{OutputCacheManager.coffee => OutputCacheManager.js} | 0 app/coffee/{OutputFileFinder.coffee => OutputFileFinder.js} | 0 app/coffee/{OutputFileOptimiser.coffee => OutputFileOptimiser.js} | 0 ...jectPersistenceManager.coffee => ProjectPersistenceManager.js} | 0 app/coffee/{RequestParser.coffee => RequestParser.js} | 0 .../{ResourceStateManager.coffee => ResourceStateManager.js} | 0 app/coffee/{ResourceWriter.coffee => ResourceWriter.js} | 0 app/coffee/{SafeReader.coffee => SafeReader.js} | 0 ...cServerForbidSymlinks.coffee => StaticServerForbidSymlinks.js} | 0 app/coffee/{TikzManager.coffee => TikzManager.js} | 0 app/coffee/{UrlCache.coffee => UrlCache.js} | 0 app/coffee/{UrlFetcher.coffee => UrlFetcher.js} | 0 app/coffee/{db.coffee => db.js} | 0 26 files changed, 0 insertions(+), 0 deletions(-) rename app/coffee/{CommandRunner.coffee => CommandRunner.js} (100%) rename app/coffee/{CompileController.coffee => CompileController.js} (100%) rename app/coffee/{CompileManager.coffee => CompileManager.js} (100%) rename app/coffee/{ContentTypeMapper.coffee => ContentTypeMapper.js} (100%) rename app/coffee/{DbQueue.coffee => DbQueue.js} (100%) rename app/coffee/{DockerLockManager.coffee => DockerLockManager.js} (100%) rename app/coffee/{DockerRunner.coffee => DockerRunner.js} (100%) rename app/coffee/{DraftModeManager.coffee => DraftModeManager.js} (100%) rename app/coffee/{Errors.coffee => Errors.js} (100%) rename app/coffee/{LatexRunner.coffee => LatexRunner.js} (100%) rename app/coffee/{LocalCommandRunner.coffee => LocalCommandRunner.js} (100%) rename app/coffee/{LockManager.coffee => LockManager.js} (100%) rename app/coffee/{Metrics.coffee => Metrics.js} (100%) rename app/coffee/{OutputCacheManager.coffee => OutputCacheManager.js} (100%) rename app/coffee/{OutputFileFinder.coffee => OutputFileFinder.js} (100%) rename app/coffee/{OutputFileOptimiser.coffee => OutputFileOptimiser.js} (100%) rename app/coffee/{ProjectPersistenceManager.coffee => ProjectPersistenceManager.js} (100%) rename app/coffee/{RequestParser.coffee => RequestParser.js} (100%) rename app/coffee/{ResourceStateManager.coffee => ResourceStateManager.js} (100%) rename app/coffee/{ResourceWriter.coffee => ResourceWriter.js} (100%) rename app/coffee/{SafeReader.coffee => SafeReader.js} (100%) rename app/coffee/{StaticServerForbidSymlinks.coffee => StaticServerForbidSymlinks.js} (100%) rename app/coffee/{TikzManager.coffee => TikzManager.js} (100%) rename app/coffee/{UrlCache.coffee => UrlCache.js} (100%) rename app/coffee/{UrlFetcher.coffee => UrlFetcher.js} (100%) rename app/coffee/{db.coffee => db.js} (100%) diff --git a/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.js similarity index 100% rename from app/coffee/CommandRunner.coffee rename to app/coffee/CommandRunner.js diff --git a/app/coffee/CompileController.coffee b/app/coffee/CompileController.js similarity index 100% rename from app/coffee/CompileController.coffee rename to app/coffee/CompileController.js diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.js similarity index 100% rename from app/coffee/CompileManager.coffee rename to app/coffee/CompileManager.js diff --git a/app/coffee/ContentTypeMapper.coffee b/app/coffee/ContentTypeMapper.js similarity index 100% rename from app/coffee/ContentTypeMapper.coffee rename to app/coffee/ContentTypeMapper.js diff --git a/app/coffee/DbQueue.coffee b/app/coffee/DbQueue.js similarity index 100% rename from app/coffee/DbQueue.coffee rename to app/coffee/DbQueue.js diff --git a/app/coffee/DockerLockManager.coffee b/app/coffee/DockerLockManager.js similarity index 100% rename from app/coffee/DockerLockManager.coffee rename to app/coffee/DockerLockManager.js diff --git a/app/coffee/DockerRunner.coffee b/app/coffee/DockerRunner.js similarity index 100% rename from app/coffee/DockerRunner.coffee rename to app/coffee/DockerRunner.js diff --git a/app/coffee/DraftModeManager.coffee b/app/coffee/DraftModeManager.js similarity index 100% rename from app/coffee/DraftModeManager.coffee rename to app/coffee/DraftModeManager.js diff --git a/app/coffee/Errors.coffee b/app/coffee/Errors.js similarity index 100% rename from app/coffee/Errors.coffee rename to app/coffee/Errors.js diff --git a/app/coffee/LatexRunner.coffee b/app/coffee/LatexRunner.js similarity index 100% rename from app/coffee/LatexRunner.coffee rename to app/coffee/LatexRunner.js diff --git a/app/coffee/LocalCommandRunner.coffee b/app/coffee/LocalCommandRunner.js similarity index 100% rename from app/coffee/LocalCommandRunner.coffee rename to app/coffee/LocalCommandRunner.js diff --git a/app/coffee/LockManager.coffee b/app/coffee/LockManager.js similarity index 100% rename from app/coffee/LockManager.coffee rename to app/coffee/LockManager.js diff --git a/app/coffee/Metrics.coffee b/app/coffee/Metrics.js similarity index 100% rename from app/coffee/Metrics.coffee rename to app/coffee/Metrics.js diff --git a/app/coffee/OutputCacheManager.coffee b/app/coffee/OutputCacheManager.js similarity index 100% rename from app/coffee/OutputCacheManager.coffee rename to app/coffee/OutputCacheManager.js diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.js similarity index 100% rename from app/coffee/OutputFileFinder.coffee rename to app/coffee/OutputFileFinder.js diff --git a/app/coffee/OutputFileOptimiser.coffee b/app/coffee/OutputFileOptimiser.js similarity index 100% rename from app/coffee/OutputFileOptimiser.coffee rename to app/coffee/OutputFileOptimiser.js diff --git a/app/coffee/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.js similarity index 100% rename from app/coffee/ProjectPersistenceManager.coffee rename to app/coffee/ProjectPersistenceManager.js diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.js similarity index 100% rename from app/coffee/RequestParser.coffee rename to app/coffee/RequestParser.js diff --git a/app/coffee/ResourceStateManager.coffee b/app/coffee/ResourceStateManager.js similarity index 100% rename from app/coffee/ResourceStateManager.coffee rename to app/coffee/ResourceStateManager.js diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.js similarity index 100% rename from app/coffee/ResourceWriter.coffee rename to app/coffee/ResourceWriter.js diff --git a/app/coffee/SafeReader.coffee b/app/coffee/SafeReader.js similarity index 100% rename from app/coffee/SafeReader.coffee rename to app/coffee/SafeReader.js diff --git a/app/coffee/StaticServerForbidSymlinks.coffee b/app/coffee/StaticServerForbidSymlinks.js similarity index 100% rename from app/coffee/StaticServerForbidSymlinks.coffee rename to app/coffee/StaticServerForbidSymlinks.js diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.js similarity index 100% rename from app/coffee/TikzManager.coffee rename to app/coffee/TikzManager.js diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.js similarity index 100% rename from app/coffee/UrlCache.coffee rename to app/coffee/UrlCache.js diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.js similarity index 100% rename from app/coffee/UrlFetcher.coffee rename to app/coffee/UrlFetcher.js diff --git a/app/coffee/db.coffee b/app/coffee/db.js similarity index 100% rename from app/coffee/db.coffee rename to app/coffee/db.js From 4655768fd21b6b3a9fae2e8093859641876504fe Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:14:14 +0100 Subject: [PATCH 471/709] decaffeinate: Convert CommandRunner.coffee and 25 other files to JS --- app/coffee/CommandRunner.js | 25 +- app/coffee/CompileController.js | 250 ++++--- app/coffee/CompileManager.js | 789 ++++++++++++---------- app/coffee/ContentTypeMapper.js | 50 +- app/coffee/DbQueue.js | 21 +- app/coffee/DockerLockManager.js | 126 ++-- app/coffee/DockerRunner.js | 825 +++++++++++++---------- app/coffee/DraftModeManager.js | 51 +- app/coffee/Errors.js | 49 +- app/coffee/LatexRunner.js | 194 +++--- app/coffee/LocalCommandRunner.js | 114 ++-- app/coffee/LockManager.js | 79 ++- app/coffee/Metrics.js | 2 +- app/coffee/OutputCacheManager.js | 469 +++++++------ app/coffee/OutputFileFinder.js | 116 ++-- app/coffee/OutputFileOptimiser.js | 122 ++-- app/coffee/ProjectPersistenceManager.js | 171 +++-- app/coffee/RequestParser.js | 270 +++++--- app/coffee/ResourceStateManager.js | 168 +++-- app/coffee/ResourceWriter.js | 322 +++++---- app/coffee/SafeReader.js | 55 +- app/coffee/StaticServerForbidSymlinks.js | 103 +-- app/coffee/TikzManager.js | 85 ++- app/coffee/UrlCache.js | 281 ++++---- app/coffee/UrlFetcher.js | 136 ++-- app/coffee/db.js | 60 +- 26 files changed, 2885 insertions(+), 2048 deletions(-) diff --git a/app/coffee/CommandRunner.js b/app/coffee/CommandRunner.js index 2d1c3a94..dd7210a3 100644 --- a/app/coffee/CommandRunner.js +++ b/app/coffee/CommandRunner.js @@ -1,11 +1,18 @@ -Settings = require "settings-sharelatex" -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let commandRunnerPath; +const Settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); -if Settings.clsi?.dockerRunner == true - commandRunnerPath = "./DockerRunner" -else - commandRunnerPath = "./LocalCommandRunner" -logger.info commandRunnerPath:commandRunnerPath, "selecting command runner for clsi" -CommandRunner = require(commandRunnerPath) +if ((Settings.clsi != null ? Settings.clsi.dockerRunner : undefined) === true) { + commandRunnerPath = "./DockerRunner"; +} else { + commandRunnerPath = "./LocalCommandRunner"; +} +logger.info({commandRunnerPath}, "selecting command runner for clsi"); +const CommandRunner = require(commandRunnerPath); -module.exports = CommandRunner +module.exports = CommandRunner; diff --git a/app/coffee/CompileController.js b/app/coffee/CompileController.js index 4952d845..cfdbcfe8 100644 --- a/app/coffee/CompileController.js +++ b/app/coffee/CompileController.js @@ -1,119 +1,163 @@ -RequestParser = require "./RequestParser" -CompileManager = require "./CompileManager" -Settings = require "settings-sharelatex" -Metrics = require "./Metrics" -ProjectPersistenceManager = require "./ProjectPersistenceManager" -logger = require "logger-sharelatex" -Errors = require "./Errors" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CompileController; +const RequestParser = require("./RequestParser"); +const CompileManager = require("./CompileManager"); +const Settings = require("settings-sharelatex"); +const Metrics = require("./Metrics"); +const ProjectPersistenceManager = require("./ProjectPersistenceManager"); +const logger = require("logger-sharelatex"); +const Errors = require("./Errors"); -module.exports = CompileController = - compile: (req, res, next = (error) ->) -> - timer = new Metrics.Timer("compile-request") - RequestParser.parse req.body, (error, request) -> - return next(error) if error? - request.project_id = req.params.project_id - request.user_id = req.params.user_id if req.params.user_id? - ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) -> - return next(error) if error? - CompileManager.doCompileWithLock request, (error, outputFiles = []) -> - if error instanceof Errors.AlreadyCompilingError - code = 423 # Http 423 Locked - status = "compile-in-progress" - else if error instanceof Errors.FilesOutOfSyncError - code = 409 # Http 409 Conflict - status = "retry" - else if error?.terminated - status = "terminated" - else if error?.validate - status = "validation-#{error.validate}" - else if error?.timedout - status = "timedout" - logger.log err: error, project_id: request.project_id, "timeout running compile" - else if error? - status = "error" - code = 500 - logger.warn err: error, project_id: request.project_id, "error running compile" - else - status = "failure" - for file in outputFiles - if file.path?.match(/output\.pdf$/) - status = "success" +module.exports = (CompileController = { + compile(req, res, next) { + if (next == null) { next = function(error) {}; } + const timer = new Metrics.Timer("compile-request"); + return RequestParser.parse(req.body, function(error, request) { + if (error != null) { return next(error); } + request.project_id = req.params.project_id; + if (req.params.user_id != null) { request.user_id = req.params.user_id; } + return ProjectPersistenceManager.markProjectAsJustAccessed(request.project_id, function(error) { + if (error != null) { return next(error); } + return CompileManager.doCompileWithLock(request, function(error, outputFiles) { + let code, status; + if (outputFiles == null) { outputFiles = []; } + if (error instanceof Errors.AlreadyCompilingError) { + code = 423; // Http 423 Locked + status = "compile-in-progress"; + } else if (error instanceof Errors.FilesOutOfSyncError) { + code = 409; // Http 409 Conflict + status = "retry"; + } else if (error != null ? error.terminated : undefined) { + status = "terminated"; + } else if (error != null ? error.validate : undefined) { + status = `validation-${error.validate}`; + } else if (error != null ? error.timedout : undefined) { + status = "timedout"; + logger.log({err: error, project_id: request.project_id}, "timeout running compile"); + } else if (error != null) { + status = "error"; + code = 500; + logger.warn({err: error, project_id: request.project_id}, "error running compile"); + } else { + let file; + status = "failure"; + for (file of Array.from(outputFiles)) { + if (file.path != null ? file.path.match(/output\.pdf$/) : undefined) { + status = "success"; + } + } - if status == "failure" - logger.warn project_id: request.project_id, outputFiles:outputFiles, "project failed to compile successfully, no output.pdf generated" + if (status === "failure") { + logger.warn({project_id: request.project_id, outputFiles}, "project failed to compile successfully, no output.pdf generated"); + } - # log an error if any core files are found - for file in outputFiles - if file.path is "core" - logger.error project_id:request.project_id, req:req, outputFiles:outputFiles, "core file found in output" - - if error? - outputFiles = error.outputFiles || [] + // log an error if any core files are found + for (file of Array.from(outputFiles)) { + if (file.path === "core") { + logger.error({project_id:request.project_id, req, outputFiles}, "core file found in output"); + } + } + } - timer.done() - res.status(code or 200).send { - compile: - status: status - error: error?.message or error - outputFiles: outputFiles.map (file) -> - url: - "#{Settings.apis.clsi.url}/project/#{request.project_id}" + - (if request.user_id? then "/user/#{request.user_id}" else "") + - (if file.build? then "/build/#{file.build}" else "") + - "/output/#{file.path}" - path: file.path - type: file.type - build: file.build + if (error != null) { + outputFiles = error.outputFiles || []; } - stopCompile: (req, res, next) -> - {project_id, user_id} = req.params - CompileManager.stopCompile project_id, user_id, (error) -> - return next(error) if error? - res.sendStatus(204) + timer.done(); + return res.status(code || 200).send({ + compile: { + status, + error: (error != null ? error.message : undefined) || error, + outputFiles: outputFiles.map(file => + ({ + url: + `${Settings.apis.clsi.url}/project/${request.project_id}` + + ((request.user_id != null) ? `/user/${request.user_id}` : "") + + ((file.build != null) ? `/build/${file.build}` : "") + + `/output/${file.path}`, + path: file.path, + type: file.type, + build: file.build + }) + ) + } + }); + }); + }); + }); + }, + + stopCompile(req, res, next) { + const {project_id, user_id} = req.params; + return CompileManager.stopCompile(project_id, user_id, function(error) { + if (error != null) { return next(error); } + return res.sendStatus(204); + }); + }, - clearCache: (req, res, next = (error) ->) -> - ProjectPersistenceManager.clearProject req.params.project_id, req.params.user_id, (error) -> - return next(error) if error? - res.sendStatus(204) # No content + clearCache(req, res, next) { + if (next == null) { next = function(error) {}; } + return ProjectPersistenceManager.clearProject(req.params.project_id, req.params.user_id, function(error) { + if (error != null) { return next(error); } + return res.sendStatus(204); + }); + }, // No content - syncFromCode: (req, res, next = (error) ->) -> - file = req.query.file - line = parseInt(req.query.line, 10) - column = parseInt(req.query.column, 10) - project_id = req.params.project_id - user_id = req.params.user_id - CompileManager.syncFromCode project_id, user_id, file, line, column, (error, pdfPositions) -> - return next(error) if error? - res.json { + syncFromCode(req, res, next) { + if (next == null) { next = function(error) {}; } + const { file } = req.query; + const line = parseInt(req.query.line, 10); + const column = parseInt(req.query.column, 10); + const { project_id } = req.params; + const { user_id } = req.params; + return CompileManager.syncFromCode(project_id, user_id, file, line, column, function(error, pdfPositions) { + if (error != null) { return next(error); } + return res.json({ pdf: pdfPositions - } + }); + }); + }, - syncFromPdf: (req, res, next = (error) ->) -> - page = parseInt(req.query.page, 10) - h = parseFloat(req.query.h) - v = parseFloat(req.query.v) - project_id = req.params.project_id - user_id = req.params.user_id - CompileManager.syncFromPdf project_id, user_id, page, h, v, (error, codePositions) -> - return next(error) if error? - res.json { + syncFromPdf(req, res, next) { + if (next == null) { next = function(error) {}; } + const page = parseInt(req.query.page, 10); + const h = parseFloat(req.query.h); + const v = parseFloat(req.query.v); + const { project_id } = req.params; + const { user_id } = req.params; + return CompileManager.syncFromPdf(project_id, user_id, page, h, v, function(error, codePositions) { + if (error != null) { return next(error); } + return res.json({ code: codePositions - } + }); + }); + }, - wordcount: (req, res, next = (error) ->) -> - file = req.query.file || "main.tex" - project_id = req.params.project_id - user_id = req.params.user_id - image = req.query.image - logger.log {image, file, project_id}, "word count request" + wordcount(req, res, next) { + if (next == null) { next = function(error) {}; } + const file = req.query.file || "main.tex"; + const { project_id } = req.params; + const { user_id } = req.params; + const { image } = req.query; + logger.log({image, file, project_id}, "word count request"); - CompileManager.wordcount project_id, user_id, file, image, (error, result) -> - return next(error) if error? - res.json { + return CompileManager.wordcount(project_id, user_id, file, image, function(error, result) { + if (error != null) { return next(error); } + return res.json({ texcount: result - } + }); + }); + }, - status: (req, res, next = (error)-> )-> - res.send("OK") + status(req, res, next ){ + if (next == null) { next = function(error){}; } + return res.send("OK"); + } +}); diff --git a/app/coffee/CompileManager.js b/app/coffee/CompileManager.js index 792beb8d..82dafd12 100644 --- a/app/coffee/CompileManager.js +++ b/app/coffee/CompileManager.js @@ -1,345 +1,454 @@ -ResourceWriter = require "./ResourceWriter" -LatexRunner = require "./LatexRunner" -OutputFileFinder = require "./OutputFileFinder" -OutputCacheManager = require "./OutputCacheManager" -Settings = require("settings-sharelatex") -Path = require "path" -logger = require "logger-sharelatex" -Metrics = require "./Metrics" -child_process = require "child_process" -DraftModeManager = require "./DraftModeManager" -TikzManager = require "./TikzManager" -LockManager = require "./LockManager" -fs = require("fs") -fse = require "fs-extra" -os = require("os") -async = require "async" -Errors = require './Errors' -CommandRunner = require "./CommandRunner" - -getCompileName = (project_id, user_id) -> - if user_id? then "#{project_id}-#{user_id}" else project_id - -getCompileDir = (project_id, user_id) -> - Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id)) - -module.exports = CompileManager = - - doCompileWithLock: (request, callback = (error, outputFiles) ->) -> - compileDir = getCompileDir(request.project_id, request.user_id) - lockFile = Path.join(compileDir, ".project-lock") - # use a .project-lock file in the compile directory to prevent - # simultaneous compiles - fse.ensureDir compileDir, (error) -> - return callback(error) if error? - LockManager.runWithLock lockFile, (releaseLock) -> - CompileManager.doCompile(request, releaseLock) - , callback - - doCompile: (request, callback = (error, outputFiles) ->) -> - 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, "syncing resources to disk" - ResourceWriter.syncResourcesToDisk request, compileDir, (error, resourceList) -> - # NOTE: resourceList is insecure, it should only be used to exclude files from the output list - if error? and error instanceof Errors.FilesOutOfSyncError - logger.warn project_id: request.project_id, user_id: request.user_id, "files out of sync, please retry" - return callback(error) - else if error? - logger.err err:error, project_id: request.project_id, user_id: request.user_id, "error writing resources to disk" - return callback(error) - logger.log project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start, "written files to disk" - timer.done() - - injectDraftModeIfRequired = (callback) -> - if request.draft - DraftModeManager.injectDraftMode Path.join(compileDir, request.rootResourcePath), callback - else - callback() - - createTikzFileIfRequired = (callback) -> - TikzManager.checkMainFile compileDir, request.rootResourcePath, resourceList, (error, needsMainFile) -> - return callback(error) if error? - if needsMainFile - TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback - else - callback() - - # set up environment variables for chktex - env = {} - # only run chktex on LaTeX files (not knitr .Rtex files or any others) - isLaTeXFile = request.rootResourcePath?.match(/\.tex$/i) - if request.check? and isLaTeXFile - env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16' - env['CHKTEX_ULIMIT_OPTIONS'] = '-t 5 -v 64000' - if request.check is 'error' - env['CHKTEX_EXIT_ON_ERROR'] = 1 - if request.check is 'validate' - env['CHKTEX_VALIDATE'] = 1 - - # apply a series of file modifications/creations for draft mode and tikz - async.series [injectDraftModeIfRequired, createTikzFileIfRequired], (error) -> - return callback(error) if error? - timer = new Metrics.Timer("run-compile") - # find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) - tag = request.imageName?.match(/:(.*)/)?[1]?.replace(/\./g,'-') or "default" - tag = "other" if not request.project_id.match(/^[0-9a-f]{24}$/) # exclude smoke test - Metrics.inc("compiles") - Metrics.inc("compiles-with-image.#{tag}") - compileName = getCompileName(request.project_id, request.user_id) - LatexRunner.runLatex compileName, { - directory: compileDir - mainFile: request.rootResourcePath - compiler: request.compiler - timeout: request.timeout - image: request.imageName - flags: request.flags +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CompileManager; +const ResourceWriter = require("./ResourceWriter"); +const LatexRunner = require("./LatexRunner"); +const OutputFileFinder = require("./OutputFileFinder"); +const OutputCacheManager = require("./OutputCacheManager"); +const Settings = require("settings-sharelatex"); +const Path = require("path"); +const logger = require("logger-sharelatex"); +const Metrics = require("./Metrics"); +const child_process = require("child_process"); +const DraftModeManager = require("./DraftModeManager"); +const TikzManager = require("./TikzManager"); +const LockManager = require("./LockManager"); +const fs = require("fs"); +const fse = require("fs-extra"); +const os = require("os"); +const async = require("async"); +const Errors = require('./Errors'); +const CommandRunner = require("./CommandRunner"); + +const getCompileName = function(project_id, user_id) { + if (user_id != null) { return `${project_id}-${user_id}`; } else { return project_id; } +}; + +const getCompileDir = (project_id, user_id) => Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id)); + +module.exports = (CompileManager = { + + doCompileWithLock(request, callback) { + if (callback == null) { callback = function(error, outputFiles) {}; } + const compileDir = getCompileDir(request.project_id, request.user_id); + const lockFile = Path.join(compileDir, ".project-lock"); + // use a .project-lock file in the compile directory to prevent + // simultaneous compiles + return fse.ensureDir(compileDir, function(error) { + if (error != null) { return callback(error); } + return LockManager.runWithLock(lockFile, releaseLock => CompileManager.doCompile(request, releaseLock) + , callback); + }); + }, + + doCompile(request, callback) { + if (callback == null) { callback = function(error, outputFiles) {}; } + const compileDir = getCompileDir(request.project_id, request.user_id); + let timer = new Metrics.Timer("write-to-disk"); + logger.log({project_id: request.project_id, user_id: request.user_id}, "syncing resources to disk"); + return ResourceWriter.syncResourcesToDisk(request, compileDir, function(error, resourceList) { + // NOTE: resourceList is insecure, it should only be used to exclude files from the output list + if ((error != null) && error instanceof Errors.FilesOutOfSyncError) { + logger.warn({project_id: request.project_id, user_id: request.user_id}, "files out of sync, please retry"); + return callback(error); + } else if (error != null) { + logger.err({err:error, project_id: request.project_id, user_id: request.user_id}, "error writing resources to disk"); + return callback(error); + } + logger.log({project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start}, "written files to disk"); + timer.done(); + + const injectDraftModeIfRequired = function(callback) { + if (request.draft) { + return DraftModeManager.injectDraftMode(Path.join(compileDir, request.rootResourcePath), callback); + } else { + return callback(); + } + }; + + const createTikzFileIfRequired = callback => + TikzManager.checkMainFile(compileDir, request.rootResourcePath, resourceList, function(error, needsMainFile) { + if (error != null) { return callback(error); } + if (needsMainFile) { + return TikzManager.injectOutputFile(compileDir, request.rootResourcePath, callback); + } else { + return callback(); + } + }) + ; + + // set up environment variables for chktex + const env = {}; + // only run chktex on LaTeX files (not knitr .Rtex files or any others) + const isLaTeXFile = request.rootResourcePath != null ? request.rootResourcePath.match(/\.tex$/i) : undefined; + if ((request.check != null) && isLaTeXFile) { + env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16'; + env['CHKTEX_ULIMIT_OPTIONS'] = '-t 5 -v 64000'; + if (request.check === 'error') { + env['CHKTEX_EXIT_ON_ERROR'] = 1; + } + if (request.check === 'validate') { + env['CHKTEX_VALIDATE'] = 1; + } + } + + // apply a series of file modifications/creations for draft mode and tikz + return async.series([injectDraftModeIfRequired, createTikzFileIfRequired], function(error) { + if (error != null) { return callback(error); } + timer = new Metrics.Timer("run-compile"); + // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) + let tag = __guard__(__guard__(request.imageName != null ? request.imageName.match(/:(.*)/) : undefined, x1 => x1[1]), x => x.replace(/\./g,'-')) || "default"; + if (!request.project_id.match(/^[0-9a-f]{24}$/)) { tag = "other"; } // exclude smoke test + Metrics.inc("compiles"); + Metrics.inc(`compiles-with-image.${tag}`); + const compileName = getCompileName(request.project_id, request.user_id); + return LatexRunner.runLatex(compileName, { + directory: compileDir, + mainFile: request.rootResourcePath, + compiler: request.compiler, + timeout: request.timeout, + image: request.imageName, + flags: request.flags, 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 - # 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 or error?.timedout - OutputFileFinder.findOutputFiles resourceList, compileDir, (err, outputFiles) -> - return callback(err) if err? - error.outputFiles = outputFiles # return output files so user can check logs - callback(error) - return - # compile completed normally - return callback(error) if error? - Metrics.inc("compiles-succeeded") - for metric_key, metric_value of stats or {} - Metrics.count(metric_key, metric_value) - for metric_key, metric_value of timings or {} - Metrics.timing(metric_key, metric_value) - loadavg = os.loadavg?() - Metrics.gauge("load-avg", loadavg[0]) if loadavg? - ts = timer.done() - logger.log {project_id: request.project_id, user_id: request.user_id, time_taken: ts, stats:stats, timings:timings, loadavg:loadavg}, "done compile" - if stats?["latex-runs"] > 0 - Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"]) - if stats?["latex-runs"] > 0 and timings?["cpu-time"] > 0 - Metrics.timing("run-compile-cpu-time-per-pass", timings["cpu-time"] / stats["latex-runs"]) - - OutputFileFinder.findOutputFiles resourceList, compileDir, (error, outputFiles) -> - return callback(error) if error? - 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) - _callback = () -> - - compileDir = getCompileDir(project_id, user_id) - - CompileManager._checkDirectory compileDir, (err, exists) -> - return callback(err) if err? - return callback() if not exists # skip removal if no directory present - - proc = child_process.spawn "rm", ["-r", compileDir] - - proc.on "error", callback - - stderr = "" - proc.stderr.on "data", (chunk) -> stderr += chunk.toString() - - proc.on "close", (code) -> - if code == 0 - return callback(null) - else - return callback(new Error("rm -r #{compileDir} failed: #{stderr}")) - - _findAllDirs: (callback = (error, allDirs) ->) -> - root = Settings.path.compilesDir - fs.readdir root, (err, files) -> - return callback(err) if err? - allDirs = (Path.join(root, file) for file in files) - callback(null, allDirs) - - clearExpiredProjects: (max_cache_age_ms, callback = (error) ->) -> - now = Date.now() - # action for each directory - expireIfNeeded = (checkDir, cb) -> - fs.stat checkDir, (err, stats) -> - return cb() if err? # ignore errors checking directory - age = now - stats.mtime - hasExpired = (age > max_cache_age_ms) - if hasExpired then fse.remove(checkDir, cb) else cb() - # iterate over all project directories - CompileManager._findAllDirs (error, allDirs) -> - return callback() if error? - async.eachSeries allDirs, expireIfNeeded, callback - - _checkDirectory: (compileDir, callback = (error, exists) ->) -> - fs.lstat compileDir, (err, stats) -> - if err?.code is 'ENOENT' - return callback(null, false) # directory does not exist - else if err? - logger.err {dir: compileDir, err:err}, "error on stat of project directory for removal" - return callback(err) - else if not stats?.isDirectory() - logger.err {dir: compileDir, stats:stats}, "bad project directory for removal" - return callback new Error("project directory is not directory") - else - callback(null, true) # directory exists - - syncFromCode: (project_id, user_id, file_name, line, column, callback = (error, pdfPositions) ->) -> - # If LaTeX was run in a virtual environment, the file path that synctex expects - # might not match the file path on the host. The .synctex.gz file however, will be accessed - # wherever it is on the host. - compileName = getCompileName(project_id, user_id) - base_dir = Settings.path.synctexBaseDir(compileName) - file_path = base_dir + "/" + file_name - compileDir = getCompileDir(project_id, user_id) - synctex_path = "#{base_dir}/output.pdf" - command = ["code", synctex_path, file_path, line, column] - fse.ensureDir compileDir, (error) -> - if error? - logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync from code" - return callback(error) - CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> - return callback(error) if error? - logger.log project_id: project_id, user_id:user_id, file_name: file_name, line: line, column: column, command:command, stdout: stdout, "synctex code output" - callback null, CompileManager._parseSynctexFromCodeOutput(stdout) - - syncFromPdf: (project_id, user_id, page, h, v, callback = (error, filePositions) ->) -> - compileName = getCompileName(project_id, user_id) - compileDir = getCompileDir(project_id, user_id) - base_dir = Settings.path.synctexBaseDir(compileName) - synctex_path = "#{base_dir}/output.pdf" - command = ["pdf", synctex_path, page, h, v] - fse.ensureDir compileDir, (error) -> - if error? - logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync to code" - return callback(error) - CompileManager._runSynctex project_id, user_id, command, (error, stdout) -> - return callback(error) if error? - logger.log project_id: project_id, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" - callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) - - _checkFileExists: (path, callback = (error) ->) -> - synctexDir = Path.dirname(path) - synctexFile = Path.join(synctexDir, "output.synctex.gz") - fs.stat synctexDir, (error, stats) -> - if error?.code is 'ENOENT' - return callback(new Errors.NotFoundError("called synctex with no output directory")) - return callback(error) if error? - fs.stat synctexFile, (error, stats) -> - if error?.code is 'ENOENT' - return callback(new Errors.NotFoundError("called synctex with no output file")) - return callback(error) if error? - return callback(new Error("not a file")) if not stats?.isFile() - callback() - - _runSynctex: (project_id, user_id, command, callback = (error, stdout) ->) -> - seconds = 1000 - - command.unshift("/opt/synctex") - - directory = getCompileDir(project_id, user_id) - timeout = 60 * 1000 # increased to allow for large projects - compileName = getCompileName(project_id, user_id) - CommandRunner.run compileName, command, directory, Settings.clsi?.docker.image, timeout, {}, (error, output) -> - if error? - logger.err err:error, command:command, project_id:project_id, user_id:user_id, "error running synctex" - return callback(error) - callback(null, output.stdout) - - _parseSynctexFromCodeOutput: (output) -> - results = [] - for line in output.split("\n") - [node, page, h, v, width, height] = line.split("\t") - if node == "NODE" - results.push { - page: parseInt(page, 10) - h: parseFloat(h) - v: parseFloat(v) - height: parseFloat(height) - width: parseFloat(width) + }, function(error, output, stats, timings) { + // request was for validation only + let metric_key, metric_value; + if (request.check === "validate") { + const result = (error != null ? error.code : undefined) ? "fail" : "pass"; + error = new Error("validation"); + error.validate = result; + } + // request was for compile, and failed on validation + if ((request.check === "error") && ((error != null ? error.message : undefined) === 'exited')) { + error = new Error("compilation"); + error.validate = "fail"; + } + // compile was killed by user, was a validation, or a compile which failed validation + if ((error != null ? error.terminated : undefined) || (error != null ? error.validate : undefined) || (error != null ? error.timedout : undefined)) { + OutputFileFinder.findOutputFiles(resourceList, compileDir, function(err, outputFiles) { + if (err != null) { return callback(err); } + error.outputFiles = outputFiles; // return output files so user can check logs + return callback(error); + }); + return; + } + // compile completed normally + if (error != null) { return callback(error); } + Metrics.inc("compiles-succeeded"); + const object = stats || {}; + for (metric_key in object) { + metric_value = object[metric_key]; + Metrics.count(metric_key, metric_value); + } + const object1 = timings || {}; + for (metric_key in object1) { + metric_value = object1[metric_key]; + Metrics.timing(metric_key, metric_value); + } + const loadavg = typeof os.loadavg === 'function' ? os.loadavg() : undefined; + if (loadavg != null) { Metrics.gauge("load-avg", loadavg[0]); } + const ts = timer.done(); + logger.log({project_id: request.project_id, user_id: request.user_id, time_taken: ts, stats, timings, loadavg}, "done compile"); + if ((stats != null ? stats["latex-runs"] : undefined) > 0) { + Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"]); + } + if (((stats != null ? stats["latex-runs"] : undefined) > 0) && ((timings != null ? timings["cpu-time"] : undefined) > 0)) { + Metrics.timing("run-compile-cpu-time-per-pass", timings["cpu-time"] / stats["latex-runs"]); + } + + return OutputFileFinder.findOutputFiles(resourceList, compileDir, function(error, outputFiles) { + if (error != null) { return callback(error); } + return OutputCacheManager.saveOutputFiles(outputFiles, compileDir, (error, newOutputFiles) => callback(null, newOutputFiles)); + }); + }); + }); + }); + }, + + stopCompile(project_id, user_id, callback) { + if (callback == null) { callback = function(error) {}; } + const compileName = getCompileName(project_id, user_id); + return LatexRunner.killLatex(compileName, callback); + }, + + clearProject(project_id, user_id, _callback) { + if (_callback == null) { _callback = function(error) {}; } + const callback = function(error) { + _callback(error); + return _callback = function() {}; + }; + + const compileDir = getCompileDir(project_id, user_id); + + return CompileManager._checkDirectory(compileDir, function(err, exists) { + if (err != null) { return callback(err); } + if (!exists) { return callback(); } // skip removal if no directory present + + const proc = child_process.spawn("rm", ["-r", compileDir]); + + proc.on("error", callback); + + let stderr = ""; + proc.stderr.on("data", chunk => stderr += chunk.toString()); + + return proc.on("close", function(code) { + if (code === 0) { + return callback(null); + } else { + return callback(new Error(`rm -r ${compileDir} failed: ${stderr}`)); } - return results - - _parseSynctexFromPdfOutput: (output, base_dir) -> - results = [] - for line in output.split("\n") - [node, file_path, line, column] = line.split("\t") - if node == "NODE" - file = file_path.slice(base_dir.length + 1) - results.push { - file: file - line: parseInt(line, 10) - column: parseInt(column, 10) + }); + }); + }, + + _findAllDirs(callback) { + if (callback == null) { callback = function(error, allDirs) {}; } + const root = Settings.path.compilesDir; + return fs.readdir(root, function(err, files) { + if (err != null) { return callback(err); } + const allDirs = (Array.from(files).map((file) => Path.join(root, file))); + return callback(null, allDirs); + }); + }, + + clearExpiredProjects(max_cache_age_ms, callback) { + if (callback == null) { callback = function(error) {}; } + const now = Date.now(); + // action for each directory + const expireIfNeeded = (checkDir, cb) => + fs.stat(checkDir, function(err, stats) { + if (err != null) { return cb(); } // ignore errors checking directory + const age = now - stats.mtime; + const hasExpired = (age > max_cache_age_ms); + if (hasExpired) { return fse.remove(checkDir, cb); } else { return cb(); } + }) + ; + // iterate over all project directories + return CompileManager._findAllDirs(function(error, allDirs) { + if (error != null) { return callback(); } + return async.eachSeries(allDirs, expireIfNeeded, callback); + }); + }, + + _checkDirectory(compileDir, callback) { + if (callback == null) { callback = function(error, exists) {}; } + return fs.lstat(compileDir, function(err, stats) { + if ((err != null ? err.code : undefined) === 'ENOENT') { + return callback(null, false); // directory does not exist + } else if (err != null) { + logger.err({dir: compileDir, err}, "error on stat of project directory for removal"); + return callback(err); + } else if (!(stats != null ? stats.isDirectory() : undefined)) { + logger.err({dir: compileDir, stats}, "bad project directory for removal"); + return callback(new Error("project directory is not directory")); + } else { + return callback(null, true); + } + }); + }, // directory exists + + syncFromCode(project_id, user_id, file_name, line, column, callback) { + // If LaTeX was run in a virtual environment, the file path that synctex expects + // might not match the file path on the host. The .synctex.gz file however, will be accessed + // wherever it is on the host. + if (callback == null) { callback = function(error, pdfPositions) {}; } + const compileName = getCompileName(project_id, user_id); + const base_dir = Settings.path.synctexBaseDir(compileName); + const file_path = base_dir + "/" + file_name; + const compileDir = getCompileDir(project_id, user_id); + const synctex_path = `${base_dir}/output.pdf`; + const command = ["code", synctex_path, file_path, line, column]; + return fse.ensureDir(compileDir, function(error) { + if (error != null) { + logger.err({error, project_id, user_id, file_name}, "error ensuring dir for sync from code"); + return callback(error); + } + return CompileManager._runSynctex(project_id, user_id, command, function(error, stdout) { + if (error != null) { return callback(error); } + logger.log({project_id, user_id, file_name, line, column, command, stdout}, "synctex code output"); + return callback(null, CompileManager._parseSynctexFromCodeOutput(stdout)); + }); + }); + }, + + syncFromPdf(project_id, user_id, page, h, v, callback) { + if (callback == null) { callback = function(error, filePositions) {}; } + const compileName = getCompileName(project_id, user_id); + const compileDir = getCompileDir(project_id, user_id); + const base_dir = Settings.path.synctexBaseDir(compileName); + const synctex_path = `${base_dir}/output.pdf`; + const command = ["pdf", synctex_path, page, h, v]; + return fse.ensureDir(compileDir, function(error) { + if (error != null) { + logger.err({error, project_id, user_id, file_name}, "error ensuring dir for sync to code"); + return callback(error); + } + return CompileManager._runSynctex(project_id, user_id, command, function(error, stdout) { + if (error != null) { return callback(error); } + logger.log({project_id, user_id, page, h, v, stdout}, "synctex pdf output"); + return callback(null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)); + }); + }); + }, + + _checkFileExists(path, callback) { + if (callback == null) { callback = function(error) {}; } + const synctexDir = Path.dirname(path); + const synctexFile = Path.join(synctexDir, "output.synctex.gz"); + return fs.stat(synctexDir, function(error, stats) { + if ((error != null ? error.code : undefined) === 'ENOENT') { + return callback(new Errors.NotFoundError("called synctex with no output directory")); + } + if (error != null) { return callback(error); } + return fs.stat(synctexFile, function(error, stats) { + if ((error != null ? error.code : undefined) === 'ENOENT') { + return callback(new Errors.NotFoundError("called synctex with no output file")); } - return results - - - 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", '-nocol', '-inc', file_path, "-out=" + file_path + ".wc"] - compileDir = getCompileDir(project_id, user_id) - timeout = 60 * 1000 - compileName = getCompileName(project_id, user_id) - fse.ensureDir compileDir, (error) -> - if error? - logger.err {error, project_id, user_id, file_name}, "error ensuring dir for sync from code" - return callback(error) - CommandRunner.run compileName, command, compileDir, image, timeout, {}, (error) -> - return callback(error) if error? - fs.readFile compileDir + "/" + file_name + ".wc", "utf-8", (err, stdout) -> - if err? - #call it node_err so sentry doesn't use random path error as unique id so it can't be ignored - logger.err node_err:err, command:command, compileDir:compileDir, project_id:project_id, user_id:user_id, "error reading word count output" - return callback(err) - 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 = { - encode: "" - textWords: 0 - headWords: 0 - outside: 0 - headers: 0 - elements: 0 - mathInline: 0 - mathDisplay: 0 - errors: 0 + if (error != null) { return callback(error); } + if (!(stats != null ? stats.isFile() : undefined)) { return callback(new Error("not a file")); } + return callback(); + }); + }); + }, + + _runSynctex(project_id, user_id, command, callback) { + if (callback == null) { callback = function(error, stdout) {}; } + const seconds = 1000; + + command.unshift("/opt/synctex"); + + const directory = getCompileDir(project_id, user_id); + const timeout = 60 * 1000; // increased to allow for large projects + const compileName = getCompileName(project_id, user_id); + return CommandRunner.run(compileName, command, directory, Settings.clsi != null ? Settings.clsi.docker.image : undefined, timeout, {}, function(error, output) { + if (error != null) { + logger.err({err:error, command, project_id, user_id}, "error running synctex"); + return callback(error); + } + return callback(null, output.stdout); + }); + }, + + _parseSynctexFromCodeOutput(output) { + const results = []; + for (let line of Array.from(output.split("\n"))) { + const [node, page, h, v, width, height] = Array.from(line.split("\t")); + if (node === "NODE") { + results.push({ + page: parseInt(page, 10), + h: parseFloat(h), + v: parseFloat(v), + height: parseFloat(height), + width: parseFloat(width) + }); + } + } + return results; + }, + + _parseSynctexFromPdfOutput(output, base_dir) { + const results = []; + for (let line of Array.from(output.split("\n"))) { + let column, file_path, node; + [node, file_path, line, column] = Array.from(line.split("\t")); + if (node === "NODE") { + const file = file_path.slice(base_dir.length + 1); + results.push({ + file, + line: parseInt(line, 10), + column: parseInt(column, 10) + }); + } + } + return results; + }, + + + wordcount(project_id, user_id, file_name, image, callback) { + if (callback == null) { callback = function(error, pdfPositions) {}; } + logger.log({project_id, user_id, file_name, image}, "running wordcount"); + const file_path = `$COMPILE_DIR/${file_name}`; + const command = [ "texcount", '-nocol', '-inc', file_path, `-out=${file_path}.wc`]; + const compileDir = getCompileDir(project_id, user_id); + const timeout = 60 * 1000; + const compileName = getCompileName(project_id, user_id); + return fse.ensureDir(compileDir, function(error) { + if (error != null) { + logger.err({error, project_id, user_id, file_name}, "error ensuring dir for sync from code"); + return callback(error); + } + return CommandRunner.run(compileName, command, compileDir, image, timeout, {}, function(error) { + if (error != null) { return callback(error); } + return fs.readFile(compileDir + "/" + file_name + ".wc", "utf-8", function(err, stdout) { + if (err != null) { + //call it node_err so sentry doesn't use random path error as unique id so it can't be ignored + logger.err({node_err:err, command, compileDir, project_id, user_id}, "error reading word count output"); + return callback(err); + } + const results = CompileManager._parseWordcountFromOutput(stdout); + logger.log({project_id, user_id, wordcount: results}, "word count results"); + return callback(null, results); + }); + }); + }); + }, + + _parseWordcountFromOutput(output) { + const results = { + encode: "", + textWords: 0, + headWords: 0, + outside: 0, + headers: 0, + elements: 0, + mathInline: 0, + mathDisplay: 0, + errors: 0, messages: "" + }; + for (let line of Array.from(output.split("\n"))) { + const [data, info] = Array.from(line.split(":")); + if (data.indexOf("Encoding") > -1) { + results['encode'] = info.trim(); + } + if (data.indexOf("in text") > -1) { + results['textWords'] = parseInt(info, 10); + } + if (data.indexOf("in head") > -1) { + results['headWords'] = parseInt(info, 10); + } + if (data.indexOf("outside") > -1) { + results['outside'] = parseInt(info, 10); + } + if (data.indexOf("of head") > -1) { + results['headers'] = parseInt(info, 10); + } + if (data.indexOf("Number of floats/tables/figures") > -1) { + results['elements'] = parseInt(info, 10); + } + if (data.indexOf("Number of math inlines") > -1) { + results['mathInline'] = parseInt(info, 10); + } + if (data.indexOf("Number of math displayed") > -1) { + results['mathDisplay'] = parseInt(info, 10); + } + if (data === "(errors") { // errors reported as (errors:123) + results['errors'] = parseInt(info, 10); + } + if (line.indexOf("!!! ") > -1) { // errors logged as !!! message !!! + results['messages'] += line + "\n"; + } } - for line in output.split("\n") - [data, info] = line.split(":") - if data.indexOf("Encoding") > -1 - results['encode'] = info.trim() - if data.indexOf("in text") > -1 - results['textWords'] = parseInt(info, 10) - if data.indexOf("in head") > -1 - results['headWords'] = parseInt(info, 10) - if data.indexOf("outside") > -1 - results['outside'] = parseInt(info, 10) - if data.indexOf("of head") > -1 - results['headers'] = parseInt(info, 10) - if data.indexOf("Number of floats/tables/figures") > -1 - results['elements'] = parseInt(info, 10) - if data.indexOf("Number of math inlines") > -1 - 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 + return results; + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/app/coffee/ContentTypeMapper.js b/app/coffee/ContentTypeMapper.js index 68b2d14f..c57057f9 100644 --- a/app/coffee/ContentTypeMapper.js +++ b/app/coffee/ContentTypeMapper.js @@ -1,24 +1,28 @@ -Path = require 'path' +let ContentTypeMapper; +const Path = require('path'); -# here we coerce html, css and js to text/plain, -# otherwise choose correct mime type based on file extension, -# falling back to octet-stream -module.exports = ContentTypeMapper = - map: (path) -> - switch Path.extname(path) - when '.txt', '.html', '.js', '.css', '.svg' - return 'text/plain' - when '.csv' - return 'text/csv' - when '.pdf' - return 'application/pdf' - when '.png' - return 'image/png' - when '.jpg', '.jpeg' - return 'image/jpeg' - when '.tiff' - return 'image/tiff' - when '.gif' - return 'image/gif' - else - return 'application/octet-stream' +// here we coerce html, css and js to text/plain, +// otherwise choose correct mime type based on file extension, +// falling back to octet-stream +module.exports = (ContentTypeMapper = { + map(path) { + switch (Path.extname(path)) { + case '.txt': case '.html': case '.js': case '.css': case '.svg': + return 'text/plain'; + case '.csv': + return 'text/csv'; + case '.pdf': + return 'application/pdf'; + case '.png': + return 'image/png'; + case '.jpg': case '.jpeg': + return 'image/jpeg'; + case '.tiff': + return 'image/tiff'; + case '.gif': + return 'image/gif'; + default: + return 'application/octet-stream'; + } + } +}); diff --git a/app/coffee/DbQueue.js b/app/coffee/DbQueue.js index a3593fdd..0f1f8cf1 100644 --- a/app/coffee/DbQueue.js +++ b/app/coffee/DbQueue.js @@ -1,13 +1,16 @@ -async = require "async" -Settings = require "settings-sharelatex" -logger = require("logger-sharelatex") -queue = async.queue((task, cb)-> - task(cb) - , Settings.parallelSqlQueryLimit) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require("async"); +const Settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); +const queue = async.queue((task, cb)=> task(cb) + , Settings.parallelSqlQueryLimit); -queue.drain = ()-> - logger.debug('all items have been processed') +queue.drain = ()=> logger.debug('all items have been processed'); module.exports = - queue: queue + {queue}; diff --git a/app/coffee/DockerLockManager.js b/app/coffee/DockerLockManager.js index bf90f023..9c7deffe 100644 --- a/app/coffee/DockerLockManager.js +++ b/app/coffee/DockerLockManager.js @@ -1,56 +1,84 @@ -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LockManager; +const logger = require("logger-sharelatex"); -LockState = {} # locks for docker container operations, by container name +const LockState = {}; // locks for docker container operations, by container name -module.exports = LockManager = +module.exports = (LockManager = { - MAX_LOCK_HOLD_TIME: 15000 # how long we can keep a lock - MAX_LOCK_WAIT_TIME: 10000 # how long we wait for a lock - LOCK_TEST_INTERVAL: 1000 # retry time + MAX_LOCK_HOLD_TIME: 15000, // how long we can keep a lock + MAX_LOCK_WAIT_TIME: 10000, // how long we wait for a lock + LOCK_TEST_INTERVAL: 1000, // retry time - tryLock: (key, callback = (err, gotLock) ->) -> - existingLock = LockState[key] - if existingLock? # the lock is already taken, check how old it is - lockAge = Date.now() - existingLock.created - if lockAge < LockManager.MAX_LOCK_HOLD_TIME - return callback(null, false) # we didn't get the lock, bail out - else - logger.error {key: key, lock: existingLock, age:lockAge}, "taking old lock by force" - # take the lock - LockState[key] = lockValue = {created: Date.now()} - callback(null, true, lockValue) + tryLock(key, callback) { + let lockValue; + if (callback == null) { callback = function(err, gotLock) {}; } + const existingLock = LockState[key]; + if (existingLock != null) { // the lock is already taken, check how old it is + const lockAge = Date.now() - existingLock.created; + if (lockAge < LockManager.MAX_LOCK_HOLD_TIME) { + return callback(null, false); // we didn't get the lock, bail out + } else { + logger.error({key, lock: existingLock, age:lockAge}, "taking old lock by force"); + } + } + // take the lock + LockState[key] = (lockValue = {created: Date.now()}); + return callback(null, true, lockValue); + }, - getLock: (key, callback = (error, lockValue) ->) -> - startTime = Date.now() - do attempt = () -> - LockManager.tryLock key, (error, gotLock, lockValue) -> - return callback(error) if error? - if gotLock - callback(null, lockValue) - else if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME - e = new Error("Lock timeout") - e.key = key - return callback(e) - else - setTimeout attempt, LockManager.LOCK_TEST_INTERVAL + getLock(key, callback) { + let attempt; + if (callback == null) { callback = function(error, lockValue) {}; } + const startTime = Date.now(); + return (attempt = () => + LockManager.tryLock(key, function(error, gotLock, lockValue) { + if (error != null) { return callback(error); } + if (gotLock) { + return callback(null, lockValue); + } else if ((Date.now() - startTime) > LockManager.MAX_LOCK_WAIT_TIME) { + const e = new Error("Lock timeout"); + e.key = key; + return callback(e); + } else { + return setTimeout(attempt, LockManager.LOCK_TEST_INTERVAL); + } + }) + )(); + }, - releaseLock: (key, lockValue, callback = (error) ->) -> - existingLock = LockState[key] - if existingLock is lockValue # lockValue is an object, so we can test by reference - delete LockState[key] # our lock, so we can free it - callback() - else if existingLock? # lock exists but doesn't match ours - logger.error {key:key, lock: existingLock}, "tried to release lock taken by force" - callback() - else - logger.error {key:key, lock: existingLock}, "tried to release lock that has gone" - callback() + releaseLock(key, lockValue, callback) { + if (callback == null) { callback = function(error) {}; } + const existingLock = LockState[key]; + if (existingLock === lockValue) { // lockValue is an object, so we can test by reference + delete LockState[key]; // our lock, so we can free it + return callback(); + } else if (existingLock != null) { // lock exists but doesn't match ours + logger.error({key, lock: existingLock}, "tried to release lock taken by force"); + return callback(); + } else { + logger.error({key, lock: existingLock}, "tried to release lock that has gone"); + return callback(); + } + }, - runWithLock: (key, runner, callback = ( (error) -> )) -> - LockManager.getLock key, (error, lockValue) -> - return callback(error) if error? - runner (error1, args...) -> - LockManager.releaseLock key, lockValue, (error2) -> - error = error1 or error2 - return callback(error) if error? - callback(null, args...) + runWithLock(key, runner, callback) { + if (callback == null) { callback = function(error) {}; } + return LockManager.getLock(key, function(error, lockValue) { + if (error != null) { return callback(error); } + return runner((error1, ...args) => + LockManager.releaseLock(key, lockValue, function(error2) { + error = error1 || error2; + if (error != null) { return callback(error); } + return callback(null, ...Array.from(args)); + }) + ); + }); + } +}); diff --git a/app/coffee/DockerRunner.js b/app/coffee/DockerRunner.js index 6ea929f2..ab78419e 100644 --- a/app/coffee/DockerRunner.js +++ b/app/coffee/DockerRunner.js @@ -1,358 +1,475 @@ -Settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -Docker = require("dockerode") -dockerode = new Docker() -crypto = require "crypto" -async = require "async" -LockManager = require "./DockerLockManager" -fs = require "fs" -Path = require 'path' -_ = require "underscore" - -logger.info "using docker runner" - -usingSiblingContainers = () -> - Settings?.path?.sandboxedCompilesHostDir? - -module.exports = DockerRunner = - ERR_NOT_DIRECTORY: new Error("not a directory") - ERR_TERMINATED: new Error("terminated") - ERR_EXITED: new Error("exited") - ERR_TIMED_OUT: new Error("container timed out") - - run: (project_id, command, directory, image, timeout, environment, callback = (error, output) ->) -> - - if usingSiblingContainers() - _newPath = Settings.path.sandboxedCompilesHostDir - logger.log {path: _newPath}, "altering bind path for sibling containers" - # Server Pro, example: - # '/var/lib/sharelatex/data/compiles/<project-id>' - # ... becomes ... - # '/opt/sharelatex_data/data/compiles/<project-id>' - directory = Path.join(Settings.path.sandboxedCompilesHostDir, Path.basename(directory)) - - volumes = {} - volumes[directory] = "/compile" - - command = (arg.toString().replace?('$COMPILE_DIR', "/compile") for arg in command) - if !image? - image = Settings.clsi.docker.image - - if Settings.texliveImageNameOveride? - img = image.split("/") - image = "#{Settings.texliveImageNameOveride}/#{img[2]}" - - options = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment) - fingerprint = DockerRunner._fingerprintContainer(options) - options.name = name = "project-#{project_id}-#{fingerprint}" - - # logOptions = _.clone(options) - # logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" - logger.log project_id: project_id, "running docker container" - DockerRunner._runAndWaitForContainer options, volumes, timeout, (error, output) -> - if error?.message?.match("HTTP code is 500") - logger.log err: error, project_id: project_id, "error running container so destroying and retrying" - DockerRunner.destroyContainer name, null, true, (error) -> - return callback(error) if error? - DockerRunner._runAndWaitForContainer options, volumes, timeout, callback - else - callback(error, output) - - return name # pass back the container name to allow it to be killed - - kill: (container_id, callback = (error) ->) -> - logger.log container_id: container_id, "sending kill signal to container" - container = dockerode.getContainer(container_id) - container.kill (error) -> - if error? and error?.message?.match?(/Cannot kill container .* is not running/) - logger.warn err: error, container_id: container_id, "container not running, continuing" - error = null - if error? - logger.error err: error, container_id: container_id, "error killing container" - return callback(error) - else - callback() - - _runAndWaitForContainer: (options, volumes, timeout, _callback = (error, output) ->) -> - callback = (args...) -> - _callback(args...) - # Only call the callback once - _callback = () -> - - name = options.name - - streamEnded = false - containerReturned = false - output = {} - - callbackIfFinished = () -> - if streamEnded and containerReturned - callback(null, output) - - attachStreamHandler = (error, _output) -> - return callback(error) if error? - output = _output - streamEnded = true - callbackIfFinished() - - DockerRunner.startContainer options, volumes, attachStreamHandler, (error, containerId) -> - return callback(error) if error? +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DockerRunner, oneHour; +const Settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); +const Docker = require("dockerode"); +const dockerode = new Docker(); +const crypto = require("crypto"); +const async = require("async"); +const LockManager = require("./DockerLockManager"); +const fs = require("fs"); +const Path = require('path'); +const _ = require("underscore"); + +logger.info("using docker runner"); + +const usingSiblingContainers = () => __guard__(Settings != null ? Settings.path : undefined, x => x.sandboxedCompilesHostDir) != null; + +module.exports = (DockerRunner = { + ERR_NOT_DIRECTORY: new Error("not a directory"), + ERR_TERMINATED: new Error("terminated"), + ERR_EXITED: new Error("exited"), + ERR_TIMED_OUT: new Error("container timed out"), + + run(project_id, command, directory, image, timeout, environment, callback) { + + let name; + if (callback == null) { callback = function(error, output) {}; } + if (usingSiblingContainers()) { + const _newPath = Settings.path.sandboxedCompilesHostDir; + logger.log({path: _newPath}, "altering bind path for sibling containers"); + // Server Pro, example: + // '/var/lib/sharelatex/data/compiles/<project-id>' + // ... becomes ... + // '/opt/sharelatex_data/data/compiles/<project-id>' + directory = Path.join(Settings.path.sandboxedCompilesHostDir, Path.basename(directory)); + } + + const volumes = {}; + volumes[directory] = "/compile"; + + command = (Array.from(command).map((arg) => __guardMethod__(arg.toString(), 'replace', o => o.replace('$COMPILE_DIR', "/compile")))); + if ((image == null)) { + ({ image } = Settings.clsi.docker); + } + + if (Settings.texliveImageNameOveride != null) { + const img = image.split("/"); + image = `${Settings.texliveImageNameOveride}/${img[2]}`; + } + + const options = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment); + const fingerprint = DockerRunner._fingerprintContainer(options); + options.name = (name = `project-${project_id}-${fingerprint}`); + + // logOptions = _.clone(options) + // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" + logger.log({project_id}, "running docker container"); + DockerRunner._runAndWaitForContainer(options, volumes, timeout, function(error, output) { + if (__guard__(error != null ? error.message : undefined, x => x.match("HTTP code is 500"))) { + logger.log({err: error, project_id}, "error running container so destroying and retrying"); + return DockerRunner.destroyContainer(name, null, true, function(error) { + if (error != null) { return callback(error); } + return DockerRunner._runAndWaitForContainer(options, volumes, timeout, callback); + }); + } else { + return callback(error, output); + } + }); + + return name; + }, // pass back the container name to allow it to be killed + + kill(container_id, callback) { + if (callback == null) { callback = function(error) {}; } + logger.log({container_id}, "sending kill signal to container"); + const container = dockerode.getContainer(container_id); + return container.kill(function(error) { + if ((error != null) && __guardMethod__(error != null ? error.message : undefined, 'match', o => o.match(/Cannot kill container .* is not running/))) { + logger.warn({err: error, container_id}, "container not running, continuing"); + error = null; + } + if (error != null) { + logger.error({err: error, container_id}, "error killing container"); + return callback(error); + } else { + return callback(); + } + }); + }, + + _runAndWaitForContainer(options, volumes, timeout, _callback) { + if (_callback == null) { _callback = function(error, output) {}; } + const callback = function(...args) { + _callback(...Array.from(args || [])); + // Only call the callback once + return _callback = function() {}; + }; + + const { name } = options; + + let streamEnded = false; + let containerReturned = false; + let output = {}; + + const callbackIfFinished = function() { + if (streamEnded && containerReturned) { + return callback(null, output); + } + }; + + const attachStreamHandler = function(error, _output) { + if (error != null) { return callback(error); } + output = _output; + streamEnded = true; + return callbackIfFinished(); + }; + + return DockerRunner.startContainer(options, volumes, attachStreamHandler, function(error, containerId) { + if (error != null) { return callback(error); } - DockerRunner.waitForContainer name, timeout, (error, exitCode) -> - return callback(error) if error? - if exitCode is 137 # exit status from kill -9 - err = DockerRunner.ERR_TERMINATED - err.terminated = true - return callback(err) - if exitCode is 1 # exit status from chktex - err = DockerRunner.ERR_EXITED - err.code = exitCode - return callback(err) - containerReturned = true - options?.HostConfig?.SecurityOpt = null #small log line - logger.log err:err, exitCode:exitCode, options:options, "docker container has exited" - callbackIfFinished() - - _getContainerOptions: (command, image, volumes, timeout, environment) -> - timeoutInSeconds = timeout / 1000 - - dockerVolumes = {} - for hostVol, dockerVol of volumes - dockerVolumes[dockerVol] = {} - - if volumes[hostVol].slice(-3).indexOf(":r") == -1 - volumes[hostVol] = "#{dockerVol}:rw" - - # merge settings and environment parameter - env = {} - for src in [Settings.clsi.docker.env, environment or {}] - env[key] = value for key, value of src - # set the path based on the image year - if m = image.match /:([0-9]+)\.[0-9]+/ - year = m[1] - else - year = "2014" - env['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/#{year}/bin/x86_64-linux/" - options = + return DockerRunner.waitForContainer(name, timeout, function(error, exitCode) { + let err; + if (error != null) { return callback(error); } + if (exitCode === 137) { // exit status from kill -9 + err = DockerRunner.ERR_TERMINATED; + err.terminated = true; + return callback(err); + } + if (exitCode === 1) { // exit status from chktex + err = DockerRunner.ERR_EXITED; + err.code = exitCode; + return callback(err); + } + containerReturned = true; + __guard__(options != null ? options.HostConfig : undefined, x => x.SecurityOpt = null); //small log line + logger.log({err, exitCode, options}, "docker container has exited"); + return callbackIfFinished(); + }); + }); + }, + + _getContainerOptions(command, image, volumes, timeout, environment) { + let m, year; + let key, value, hostVol, dockerVol; + const timeoutInSeconds = timeout / 1000; + + const dockerVolumes = {}; + for (hostVol in volumes) { + dockerVol = volumes[hostVol]; + dockerVolumes[dockerVol] = {}; + + if (volumes[hostVol].slice(-3).indexOf(":r") === -1) { + volumes[hostVol] = `${dockerVol}:rw`; + } + } + + // merge settings and environment parameter + const env = {}; + for (let src of [Settings.clsi.docker.env, environment || {}]) { + for (key in src) { value = src[key]; env[key] = value; } + } + // set the path based on the image year + if ((m = image.match(/:([0-9]+)\.[0-9]+/))) { + year = m[1]; + } else { + year = "2014"; + } + env['PATH'] = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/`; + const options = { "Cmd" : command, - "Image" : image - "Volumes" : dockerVolumes - "WorkingDir" : "/compile" - "NetworkDisabled" : true - "Memory" : 1024 * 1024 * 1024 * 1024 # 1 Gb - "User" : Settings.clsi.docker.user - "Env" : ("#{key}=#{value}" for key, value of env) # convert the environment hash to an array - "HostConfig" : - "Binds": ("#{hostVol}:#{dockerVol}" for hostVol, dockerVol of volumes) - "LogConfig": {"Type": "none", "Config": {}} - "Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}] - "CapDrop": "ALL" + "Image" : image, + "Volumes" : dockerVolumes, + "WorkingDir" : "/compile", + "NetworkDisabled" : true, + "Memory" : 1024 * 1024 * 1024 * 1024, // 1 Gb + "User" : Settings.clsi.docker.user, + "Env" : (((() => { + const result = []; + for (key in env) { + value = env[key]; + result.push(`${key}=${value}`); + } + return result; + })())), // convert the environment hash to an array + "HostConfig" : { + "Binds": (((() => { + const result1 = []; + for (hostVol in volumes) { + dockerVol = volumes[hostVol]; + result1.push(`${hostVol}:${dockerVol}`); + } + return result1; + })())), + "LogConfig": {"Type": "none", "Config": {}}, + "Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}], + "CapDrop": "ALL", "SecurityOpt": ["no-new-privileges"] - - - if Settings.path?.synctexBinHostPath? - options["HostConfig"]["Binds"].push("#{Settings.path.synctexBinHostPath}:/opt/synctex:ro") - - if Settings.clsi.docker.seccomp_profile? - options.HostConfig.SecurityOpt.push "seccomp=#{Settings.clsi.docker.seccomp_profile}" - - return options - - _fingerprintContainer: (containerOptions) -> - # Yay, Hashing! - json = JSON.stringify(containerOptions) - return crypto.createHash("md5").update(json).digest("hex") - - startContainer: (options, volumes, attachStreamHandler, callback) -> - LockManager.runWithLock options.name, (releaseLock) -> - # Check that volumes exist before starting the container. - # When a container is started with volume pointing to a - # non-existent directory then docker creates the directory but - # with root ownership. - DockerRunner._checkVolumes options, volumes, (err) -> - return releaseLock(err) if err? - DockerRunner._startContainer options, volumes, attachStreamHandler, releaseLock - , callback - - # Check that volumes exist and are directories - _checkVolumes: (options, volumes, callback = (error, containerName) ->) -> - if usingSiblingContainers() - # Server Pro, with sibling-containers active, skip checks - return callback(null) - - checkVolume = (path, cb) -> - fs.stat path, (err, stats) -> - return cb(err) if err? - return cb(DockerRunner.ERR_NOT_DIRECTORY) if not stats?.isDirectory() - cb() - jobs = [] - for vol of volumes - do (vol) -> - jobs.push (cb) -> checkVolume(vol, cb) - async.series jobs, callback - - _startContainer: (options, volumes, attachStreamHandler, callback = ((error, output) ->)) -> - callback = _.once(callback) - name = options.name - - logger.log {container_name: name}, "starting container" - container = dockerode.getContainer(name) - - createAndStartContainer = -> - dockerode.createContainer options, (error, container) -> - return callback(error) if error? - startExistingContainer() - - startExistingContainer = -> - DockerRunner.attachToContainer options.name, attachStreamHandler, (error)-> - return callback(error) if error? - container.start (error) -> - if error? and error?.statusCode != 304 #already running - return callback(error) - else - callback() - - container.inspect (error, stats)-> - if error?.statusCode == 404 - createAndStartContainer() - else if error? - logger.err {container_name: name, error:error}, "unable to inspect container to start" - return callback(error) - else - startExistingContainer() - - - attachToContainer: (containerId, attachStreamHandler, attachStartCallback) -> - container = dockerode.getContainer(containerId) - container.attach {stdout: 1, stderr: 1, stream: 1}, (error, stream) -> - if error? - logger.error err: error, container_id: containerId, "error attaching to container" - return attachStartCallback(error) - else - attachStartCallback() - - - logger.log container_id: containerId, "attached to container" - - MAX_OUTPUT = 1024 * 1024 # limit output to 1MB - createStringOutputStream = (name) -> + } + }; + + + if ((Settings.path != null ? Settings.path.synctexBinHostPath : undefined) != null) { + options["HostConfig"]["Binds"].push(`${Settings.path.synctexBinHostPath}:/opt/synctex:ro`); + } + + if (Settings.clsi.docker.seccomp_profile != null) { + options.HostConfig.SecurityOpt.push(`seccomp=${Settings.clsi.docker.seccomp_profile}`); + } + + return options; + }, + + _fingerprintContainer(containerOptions) { + // Yay, Hashing! + const json = JSON.stringify(containerOptions); + return crypto.createHash("md5").update(json).digest("hex"); + }, + + startContainer(options, volumes, attachStreamHandler, callback) { + return LockManager.runWithLock(options.name, releaseLock => + // Check that volumes exist before starting the container. + // When a container is started with volume pointing to a + // non-existent directory then docker creates the directory but + // with root ownership. + DockerRunner._checkVolumes(options, volumes, function(err) { + if (err != null) { return releaseLock(err); } + return DockerRunner._startContainer(options, volumes, attachStreamHandler, releaseLock); + }) + + , callback); + }, + + // Check that volumes exist and are directories + _checkVolumes(options, volumes, callback) { + if (callback == null) { callback = function(error, containerName) {}; } + if (usingSiblingContainers()) { + // Server Pro, with sibling-containers active, skip checks + return callback(null); + } + + const checkVolume = (path, cb) => + fs.stat(path, function(err, stats) { + if (err != null) { return cb(err); } + if (!(stats != null ? stats.isDirectory() : undefined)) { return cb(DockerRunner.ERR_NOT_DIRECTORY); } + return cb(); + }) + ; + const jobs = []; + for (let vol in volumes) { + (vol => jobs.push(cb => checkVolume(vol, cb)))(vol); + } + return async.series(jobs, callback); + }, + + _startContainer(options, volumes, attachStreamHandler, callback) { + if (callback == null) { callback = function(error, output) {}; } + callback = _.once(callback); + const { name } = options; + + logger.log({container_name: name}, "starting container"); + const container = dockerode.getContainer(name); + + const createAndStartContainer = () => + dockerode.createContainer(options, function(error, container) { + if (error != null) { return callback(error); } + return startExistingContainer(); + }) + ; + + var startExistingContainer = () => + DockerRunner.attachToContainer(options.name, attachStreamHandler, function(error){ + if (error != null) { return callback(error); } + return container.start(function(error) { + if ((error != null) && ((error != null ? error.statusCode : undefined) !== 304)) { //already running + return callback(error); + } else { + return callback(); + } + }); + }) + ; + + return container.inspect(function(error, stats){ + if ((error != null ? error.statusCode : undefined) === 404) { + return createAndStartContainer(); + } else if (error != null) { + logger.err({container_name: name, error}, "unable to inspect container to start"); + return callback(error); + } else { + return startExistingContainer(); + } + }); + }, + + + attachToContainer(containerId, attachStreamHandler, attachStartCallback) { + const container = dockerode.getContainer(containerId); + return container.attach({stdout: 1, stderr: 1, stream: 1}, function(error, stream) { + if (error != null) { + logger.error({err: error, container_id: containerId}, "error attaching to container"); + return attachStartCallback(error); + } else { + attachStartCallback(); + } + + + logger.log({container_id: containerId}, "attached to container"); + + const MAX_OUTPUT = 1024 * 1024; // limit output to 1MB + const createStringOutputStream = function(name) { return { - data: "" - overflowed: false - write: (data) -> - return if @overflowed - if @data.length < MAX_OUTPUT - @data += data - else - logger.error container_id: containerId, length: @data.length, maxLen: MAX_OUTPUT, "#{name} exceeds max size" - @data += "(...truncated at #{MAX_OUTPUT} chars...)" - @overflowed = true - # kill container if too much output - # docker.containers.kill(containerId, () ->) - } - - stdout = createStringOutputStream "stdout" - stderr = createStringOutputStream "stderr" - - container.modem.demuxStream(stream, stdout, stderr) - - stream.on "error", (err) -> - logger.error err: err, container_id: containerId, "error reading from container stream" - - stream.on "end", () -> - attachStreamHandler null, {stdout: stdout.data, stderr: stderr.data} - - waitForContainer: (containerId, timeout, _callback = (error, exitCode) ->) -> - callback = (args...) -> - _callback(args...) - # Only call the callback once - _callback = () -> - - container = dockerode.getContainer(containerId) - - timedOut = false - timeoutId = setTimeout () -> - timedOut = true - logger.log container_id: containerId, "timeout reached, killing container" - container.kill(() ->) - , timeout - - logger.log container_id: containerId, "waiting for docker container" - container.wait (error, res) -> - if error? - clearTimeout timeoutId - logger.error err: error, container_id: containerId, "error waiting for container" - return callback(error) - if timedOut - logger.log containerId: containerId, "docker container timed out" - error = DockerRunner.ERR_TIMED_OUT - error.timedout = true - callback error - else - clearTimeout timeoutId - logger.log container_id: containerId, exitCode: res.StatusCode, "docker container returned" - callback null, res.StatusCode - - destroyContainer: (containerName, containerId, shouldForce, callback = (error) ->) -> - # We want the containerName for the lock and, ideally, the - # containerId to delete. There is a bug in the docker.io module - # where if you delete by name and there is an error, it throws an - # async exception, but if you delete by id it just does a normal - # error callback. We fall back to deleting by name if no id is - # supplied. - LockManager.runWithLock containerName, (releaseLock) -> - DockerRunner._destroyContainer containerId or containerName, shouldForce, releaseLock - , callback - - _destroyContainer: (containerId, shouldForce, callback = (error) ->) -> - logger.log container_id: containerId, "destroying docker container" - container = dockerode.getContainer(containerId) - container.remove {force: shouldForce == true}, (error) -> - if error? and error?.statusCode == 404 - logger.warn err: error, container_id: containerId, "container not found, continuing" - error = null - if error? - logger.error err: error, container_id: containerId, "error destroying container" - else - logger.log container_id: containerId, "destroyed container" - callback(error) - - # handle expiry of docker containers - - MAX_CONTAINER_AGE: Settings.clsi.docker.maxContainerAge or oneHour = 60 * 60 * 1000 - - examineOldContainer: (container, callback = (error, name, id, ttl)->) -> - name = container.Name or container.Names?[0] - created = container.Created * 1000 # creation time is returned in seconds - now = Date.now() - age = now - created - maxAge = DockerRunner.MAX_CONTAINER_AGE - ttl = maxAge - age - logger.log {containerName: name, created: created, now: now, age: age, maxAge: maxAge, ttl: ttl}, "checking whether to destroy container" - callback(null, name, container.Id, ttl) - - destroyOldContainers: (callback = (error) ->) -> - dockerode.listContainers all: true, (error, containers) -> - return callback(error) if error? - jobs = [] - for container in containers or [] - do (container) -> - DockerRunner.examineOldContainer container, (err, name, id, ttl) -> - if name.slice(0, 9) == '/project-' && ttl <= 0 - jobs.push (cb) -> - DockerRunner.destroyContainer name, id, false, () -> cb() - # Ignore errors because some containers get stuck but - # will be destroyed next time - async.series jobs, callback - - startContainerMonitor: () -> - logger.log {maxAge: DockerRunner.MAX_CONTAINER_AGE}, "starting container expiry" - # randomise the start time - randomDelay = Math.floor(Math.random() * 5 * 60 * 1000) - setTimeout () -> - setInterval () -> - DockerRunner.destroyOldContainers() - , oneHour = 60 * 60 * 1000 - , randomDelay - -DockerRunner.startContainerMonitor() + data: "", + overflowed: false, + write(data) { + if (this.overflowed) { return; } + if (this.data.length < MAX_OUTPUT) { + return this.data += data; + } else { + logger.error({container_id: containerId, length: this.data.length, maxLen: MAX_OUTPUT}, `${name} exceeds max size`); + this.data += `(...truncated at ${MAX_OUTPUT} chars...)`; + return this.overflowed = true; + } + } + // kill container if too much output + // docker.containers.kill(containerId, () ->) + }; + }; + + const stdout = createStringOutputStream("stdout"); + const stderr = createStringOutputStream("stderr"); + + container.modem.demuxStream(stream, stdout, stderr); + + stream.on("error", err => logger.error({err, container_id: containerId}, "error reading from container stream")); + + return stream.on("end", () => attachStreamHandler(null, {stdout: stdout.data, stderr: stderr.data})); + }); + }, + + waitForContainer(containerId, timeout, _callback) { + if (_callback == null) { _callback = function(error, exitCode) {}; } + const callback = function(...args) { + _callback(...Array.from(args || [])); + // Only call the callback once + return _callback = function() {}; + }; + + const container = dockerode.getContainer(containerId); + + let timedOut = false; + const timeoutId = setTimeout(function() { + timedOut = true; + logger.log({container_id: containerId}, "timeout reached, killing container"); + return container.kill(function() {}); + } + , timeout); + + logger.log({container_id: containerId}, "waiting for docker container"); + return container.wait(function(error, res) { + if (error != null) { + clearTimeout(timeoutId); + logger.error({err: error, container_id: containerId}, "error waiting for container"); + return callback(error); + } + if (timedOut) { + logger.log({containerId}, "docker container timed out"); + error = DockerRunner.ERR_TIMED_OUT; + error.timedout = true; + return callback(error); + } else { + clearTimeout(timeoutId); + logger.log({container_id: containerId, exitCode: res.StatusCode}, "docker container returned"); + return callback(null, res.StatusCode); + } + }); + }, + + destroyContainer(containerName, containerId, shouldForce, callback) { + // We want the containerName for the lock and, ideally, the + // containerId to delete. There is a bug in the docker.io module + // where if you delete by name and there is an error, it throws an + // async exception, but if you delete by id it just does a normal + // error callback. We fall back to deleting by name if no id is + // supplied. + if (callback == null) { callback = function(error) {}; } + return LockManager.runWithLock(containerName, releaseLock => DockerRunner._destroyContainer(containerId || containerName, shouldForce, releaseLock) + , callback); + }, + + _destroyContainer(containerId, shouldForce, callback) { + if (callback == null) { callback = function(error) {}; } + logger.log({container_id: containerId}, "destroying docker container"); + const container = dockerode.getContainer(containerId); + return container.remove({force: shouldForce === true}, function(error) { + if ((error != null) && ((error != null ? error.statusCode : undefined) === 404)) { + logger.warn({err: error, container_id: containerId}, "container not found, continuing"); + error = null; + } + if (error != null) { + logger.error({err: error, container_id: containerId}, "error destroying container"); + } else { + logger.log({container_id: containerId}, "destroyed container"); + } + return callback(error); + }); + }, + + // handle expiry of docker containers + + MAX_CONTAINER_AGE: Settings.clsi.docker.maxContainerAge || (oneHour = 60 * 60 * 1000), + + examineOldContainer(container, callback) { + if (callback == null) { callback = function(error, name, id, ttl){}; } + const name = container.Name || (container.Names != null ? container.Names[0] : undefined); + const created = container.Created * 1000; // creation time is returned in seconds + const now = Date.now(); + const age = now - created; + const maxAge = DockerRunner.MAX_CONTAINER_AGE; + const ttl = maxAge - age; + logger.log({containerName: name, created, now, age, maxAge, ttl}, "checking whether to destroy container"); + return callback(null, name, container.Id, ttl); + }, + + destroyOldContainers(callback) { + if (callback == null) { callback = function(error) {}; } + return dockerode.listContainers({all: true}, function(error, containers) { + if (error != null) { return callback(error); } + const jobs = []; + for (let container of Array.from(containers || [])) { + (container => + DockerRunner.examineOldContainer(container, function(err, name, id, ttl) { + if ((name.slice(0, 9) === '/project-') && (ttl <= 0)) { + return jobs.push(cb => DockerRunner.destroyContainer(name, id, false, () => cb())); + } + }) + )(container); + } + // Ignore errors because some containers get stuck but + // will be destroyed next time + return async.series(jobs, callback); + }); + }, + + startContainerMonitor() { + logger.log({maxAge: DockerRunner.MAX_CONTAINER_AGE}, "starting container expiry"); + // randomise the start time + const randomDelay = Math.floor(Math.random() * 5 * 60 * 1000); + return setTimeout(() => + setInterval(() => DockerRunner.destroyOldContainers() + , (oneHour = 60 * 60 * 1000)) + + , randomDelay); + } +}); + +DockerRunner.startContainerMonitor(); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} +function __guardMethod__(obj, methodName, transform) { + if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { + return transform(obj, methodName); + } else { + return undefined; + } +} \ No newline at end of file diff --git a/app/coffee/DraftModeManager.js b/app/coffee/DraftModeManager.js index 2f9e931c..8ddbbd02 100644 --- a/app/coffee/DraftModeManager.js +++ b/app/coffee/DraftModeManager.js @@ -1,24 +1,37 @@ -fs = require "fs" -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DraftModeManager; +const fs = require("fs"); +const logger = require("logger-sharelatex"); -module.exports = DraftModeManager = - injectDraftMode: (filename, callback = (error) ->) -> - fs.readFile filename, "utf8", (error, content) -> - return callback(error) if error? - # avoid adding draft mode more than once - if content?.indexOf("\\documentclass\[draft") >= 0 - return callback() - modified_content = DraftModeManager._injectDraftOption content - logger.log { - content: content.slice(0,1024), # \documentclass is normally v near the top +module.exports = (DraftModeManager = { + injectDraftMode(filename, callback) { + if (callback == null) { callback = function(error) {}; } + return fs.readFile(filename, "utf8", function(error, content) { + if (error != null) { return callback(error); } + // avoid adding draft mode more than once + if ((content != null ? content.indexOf("\\documentclass\[draft") : undefined) >= 0) { + return callback(); + } + const modified_content = DraftModeManager._injectDraftOption(content); + logger.log({ + content: content.slice(0,1024), // \documentclass is normally v near the top modified_content: modified_content.slice(0,1024), filename - }, "injected draft class" - fs.writeFile filename, modified_content, callback + }, "injected draft class"); + return fs.writeFile(filename, modified_content, callback); + }); + }, - _injectDraftOption: (content) -> - content - # With existing options (must be first, otherwise both are applied) + _injectDraftOption(content) { + return content + // With existing options (must be first, otherwise both are applied) .replace(/\\documentclass\[/g, "\\documentclass[draft,") - # Without existing options - .replace(/\\documentclass\{/g, "\\documentclass[draft]{") + // Without existing options + .replace(/\\documentclass\{/g, "\\documentclass[draft]{"); + } +}); diff --git a/app/coffee/Errors.js b/app/coffee/Errors.js index b375513e..3a9ef220 100644 --- a/app/coffee/Errors.js +++ b/app/coffee/Errors.js @@ -1,25 +1,30 @@ -NotFoundError = (message) -> - error = new Error(message) - error.name = "NotFoundError" - error.__proto__ = NotFoundError.prototype - return error -NotFoundError.prototype.__proto__ = Error.prototype +let Errors; +var NotFoundError = function(message) { + const error = new Error(message); + error.name = "NotFoundError"; + error.__proto__ = NotFoundError.prototype; + return error; +}; +NotFoundError.prototype.__proto__ = Error.prototype; -FilesOutOfSyncError = (message) -> - error = new Error(message) - error.name = "FilesOutOfSyncError" - error.__proto__ = FilesOutOfSyncError.prototype - return error -FilesOutOfSyncError.prototype.__proto__ = Error.prototype +var FilesOutOfSyncError = function(message) { + const error = new Error(message); + error.name = "FilesOutOfSyncError"; + error.__proto__ = FilesOutOfSyncError.prototype; + return error; +}; +FilesOutOfSyncError.prototype.__proto__ = Error.prototype; -AlreadyCompilingError = (message) -> - error = new Error(message) - error.name = "AlreadyCompilingError" - error.__proto__ = AlreadyCompilingError.prototype - return error -AlreadyCompilingError.prototype.__proto__ = Error.prototype +var AlreadyCompilingError = function(message) { + const error = new Error(message); + error.name = "AlreadyCompilingError"; + error.__proto__ = AlreadyCompilingError.prototype; + return error; +}; +AlreadyCompilingError.prototype.__proto__ = Error.prototype; -module.exports = Errors = - NotFoundError: NotFoundError - FilesOutOfSyncError: FilesOutOfSyncError - AlreadyCompilingError: AlreadyCompilingError +module.exports = (Errors = { + NotFoundError, + FilesOutOfSyncError, + AlreadyCompilingError +}); diff --git a/app/coffee/LatexRunner.js b/app/coffee/LatexRunner.js index 29433f83..4c83e084 100644 --- a/app/coffee/LatexRunner.js +++ b/app/coffee/LatexRunner.js @@ -1,95 +1,123 @@ -Path = require "path" -Settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -Metrics = require "./Metrics" -CommandRunner = require "./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, environment, flags} = options - compiler ||= "pdflatex" - timeout ||= 60000 # milliseconds - - logger.log directory: directory, compiler: compiler, timeout: timeout, mainFile: mainFile, environment: environment, flags:flags, "starting compile" - - # We want to run latexmk on the tex file which we will automatically - # generate from the Rtex/Rmd/md file. - mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, ".tex") - - if compiler == "pdflatex" - command = LatexRunner._pdflatexCommand mainFile, flags - else if compiler == "latex" - command = LatexRunner._latexCommand mainFile, flags - else if compiler == "xelatex" - command = LatexRunner._xelatexCommand mainFile, flags - else if compiler == "lualatex" - command = LatexRunner._lualatexCommand mainFile, flags - else - return callback new Error("unknown compiler: #{compiler}") - - if Settings.clsi?.strace - command = ["strace", "-o", "strace", "-ff"].concat(command) - - id = "#{project_id}" # record running project under this id - - 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 - failed = if output?.stdout?.match(/^Latexmk: Errors/m)? then 1 else 0 - # counters from latexmk output - stats = {} - stats["latexmk-errors"] = failed - stats["latex-runs"] = runs - stats["latex-runs-with-errors"] = if failed then runs else 0 - stats["latex-runs-#{runs}"] = 1 - stats["latex-runs-with-errors-#{runs}"] = if failed then 1 else 0 - # timing information from /usr/bin/time - timings = {} - stderr = output?.stderr - timings["cpu-percent"] = stderr?.match(/Percent of CPU this job got: (\d+)/m)?[1] or 0 - timings["cpu-time"] = stderr?.match(/User time.*: (\d+.\d+)/m)?[1] or 0 - 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]? - logger.warn {id}, "no such project to kill" - return callback(null) - else - CommandRunner.kill ProcessTable[id], callback - - _latexmkBaseCommand: (flags) -> - args = ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR", "-synctex=1","-interaction=batchmode"] - if flags - args = args.concat(flags) - (Settings?.clsi?.latexmkCommandPrefix || []).concat(args) - - _pdflatexCommand: (mainFile, flags) -> - LatexRunner._latexmkBaseCommand(flags).concat [ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LatexRunner; +const Path = require("path"); +const Settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); +const Metrics = require("./Metrics"); +const CommandRunner = require("./CommandRunner"); + +const ProcessTable = {}; // table of currently running jobs (pids or docker container names) + +module.exports = (LatexRunner = { + runLatex(project_id, options, callback) { + let command; + if (callback == null) { callback = function(error) {}; } + let {directory, mainFile, compiler, timeout, image, environment, flags} = options; + if (!compiler) { compiler = "pdflatex"; } + if (!timeout) { timeout = 60000; } // milliseconds + + logger.log({directory, compiler, timeout, mainFile, environment, flags}, "starting compile"); + + // We want to run latexmk on the tex file which we will automatically + // generate from the Rtex/Rmd/md file. + mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, ".tex"); + + if (compiler === "pdflatex") { + command = LatexRunner._pdflatexCommand(mainFile, flags); + } else if (compiler === "latex") { + command = LatexRunner._latexCommand(mainFile, flags); + } else if (compiler === "xelatex") { + command = LatexRunner._xelatexCommand(mainFile, flags); + } else if (compiler === "lualatex") { + command = LatexRunner._lualatexCommand(mainFile, flags); + } else { + return callback(new Error(`unknown compiler: ${compiler}`)); + } + + if (Settings.clsi != null ? Settings.clsi.strace : undefined) { + command = ["strace", "-o", "strace", "-ff"].concat(command); + } + + const id = `${project_id}`; // record running project under this id + + return ProcessTable[id] = CommandRunner.run(project_id, command, directory, image, timeout, environment, function(error, output) { + delete ProcessTable[id]; + if (error != null) { return callback(error); } + const runs = __guard__(__guard__(output != null ? output.stderr : undefined, x1 => x1.match(/^Run number \d+ of .*latex/mg)), x => x.length) || 0; + const failed = (__guard__(output != null ? output.stdout : undefined, x2 => x2.match(/^Latexmk: Errors/m)) != null) ? 1 : 0; + // counters from latexmk output + const stats = {}; + stats["latexmk-errors"] = failed; + stats["latex-runs"] = runs; + stats["latex-runs-with-errors"] = failed ? runs : 0; + stats[`latex-runs-${runs}`] = 1; + stats[`latex-runs-with-errors-${runs}`] = failed ? 1 : 0; + // timing information from /usr/bin/time + const timings = {}; + const stderr = output != null ? output.stderr : undefined; + timings["cpu-percent"] = __guard__(stderr != null ? stderr.match(/Percent of CPU this job got: (\d+)/m) : undefined, x3 => x3[1]) || 0; + timings["cpu-time"] = __guard__(stderr != null ? stderr.match(/User time.*: (\d+.\d+)/m) : undefined, x4 => x4[1]) || 0; + timings["sys-time"] = __guard__(stderr != null ? stderr.match(/System time.*: (\d+.\d+)/m) : undefined, x5 => x5[1]) || 0; + return callback(error, output, stats, timings); + }); + }, + + killLatex(project_id, callback) { + if (callback == null) { callback = function(error) {}; } + const id = `${project_id}`; + logger.log({id}, "killing running compile"); + if ((ProcessTable[id] == null)) { + logger.warn({id}, "no such project to kill"); + return callback(null); + } else { + return CommandRunner.kill(ProcessTable[id], callback); + } + }, + + _latexmkBaseCommand(flags) { + let args = ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR", "-synctex=1","-interaction=batchmode"]; + if (flags) { + args = args.concat(flags); + } + return (__guard__(Settings != null ? Settings.clsi : undefined, x => x.latexmkCommandPrefix) || []).concat(args); + }, + + _pdflatexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ "-pdf", Path.join("$COMPILE_DIR", mainFile) - ] + ]); + }, - _latexCommand: (mainFile, flags) -> - LatexRunner._latexmkBaseCommand(flags).concat [ + _latexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ "-pdfdvi", Path.join("$COMPILE_DIR", mainFile) - ] + ]); + }, - _xelatexCommand: (mainFile, flags) -> - LatexRunner._latexmkBaseCommand(flags).concat [ + _xelatexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ "-xelatex", Path.join("$COMPILE_DIR", mainFile) - ] + ]); + }, - _lualatexCommand: (mainFile, flags) -> - LatexRunner._latexmkBaseCommand(flags).concat [ + _lualatexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ "-lualatex", Path.join("$COMPILE_DIR", mainFile) - ] + ]); + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/app/coffee/LocalCommandRunner.js b/app/coffee/LocalCommandRunner.js index c5ef3c69..405c51bd 100644 --- a/app/coffee/LocalCommandRunner.js +++ b/app/coffee/LocalCommandRunner.js @@ -1,48 +1,66 @@ -spawn = require("child_process").spawn -logger = require "logger-sharelatex" - -logger.info "using standard command runner" - -module.exports = CommandRunner = - run: (project_id, command, directory, image, timeout, environment, callback = (error) ->) -> - command = (arg.toString().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), cwd: directory, env: env - - stdout = "" - proc.stdout.on "data", (data)-> - stdout += data - - proc.on "error", (err)-> - logger.err err:err, project_id:project_id, command: command, directory: directory, "error running command" - callback(err) - - 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 if code is 1 # exit status from chktex - err = new Error("exited") - err.code = code - return callback(err) - else - callback(null, {"stdout": stdout}) - - 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() +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CommandRunner; +const { spawn } = require("child_process"); +const logger = require("logger-sharelatex"); + +logger.info("using standard command runner"); + +module.exports = (CommandRunner = { + run(project_id, command, directory, image, timeout, environment, callback) { + let key, value; + if (callback == null) { callback = function(error) {}; } + command = (Array.from(command).map((arg) => arg.toString().replace('$COMPILE_DIR', directory))); + logger.log({project_id, command, directory}, "running command"); + logger.warn("timeouts and sandboxing are not enabled with CommandRunner"); + + // merge environment settings + const env = {}; + for (key in process.env) { value = process.env[key]; env[key] = value; } + for (key in environment) { value = environment[key]; env[key] = value; } + + // run command as detached process so it has its own process group (which can be killed if needed) + const proc = spawn(command[0], command.slice(1), {cwd: directory, env}); + + let stdout = ""; + proc.stdout.on("data", data=> stdout += data); + + proc.on("error", function(err){ + logger.err({err, project_id, command, directory}, "error running command"); + return callback(err); + }); + + proc.on("close", function(code, signal) { + let err; + logger.info({code, signal, project_id}, "command exited"); + if (signal === 'SIGTERM') { // signal from kill method below + err = new Error("terminated"); + err.terminated = true; + return callback(err); + } else if (code === 1) { // exit status from chktex + err = new Error("exited"); + err.code = code; + return callback(err); + } else { + return callback(null, {"stdout": stdout}); + } + }); + + return proc.pid; + }, // return process id to allow job to be killed if necessary + + kill(pid, callback) { + if (callback == null) { callback = function(error) {}; } + try { + process.kill(-pid); // kill all processes in group + } catch (err) { + return callback(err); + } + return callback(); + } +}); diff --git a/app/coffee/LockManager.js b/app/coffee/LockManager.js index 5d9fe26a..2405e8ac 100644 --- a/app/coffee/LockManager.js +++ b/app/coffee/LockManager.js @@ -1,31 +1,50 @@ -Settings = require('settings-sharelatex') -logger = require "logger-sharelatex" -Lockfile = require('lockfile') # from https://github.com/npm/lockfile -Errors = require "./Errors" -fs = require("fs") -Path = require("path") -module.exports = LockManager = - LOCK_TEST_INTERVAL: 1000 # 50ms between each test of the lock - MAX_LOCK_WAIT_TIME: 15000 # 10s maximum time to spend trying to get the lock - LOCK_STALE: 5*60*1000 # 5 mins time until lock auto expires +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LockManager; +const Settings = require('settings-sharelatex'); +const logger = require("logger-sharelatex"); +const Lockfile = require('lockfile'); // from https://github.com/npm/lockfile +const Errors = require("./Errors"); +const fs = require("fs"); +const Path = require("path"); +module.exports = (LockManager = { + LOCK_TEST_INTERVAL: 1000, // 50ms between each test of the lock + MAX_LOCK_WAIT_TIME: 15000, // 10s maximum time to spend trying to get the lock + LOCK_STALE: 5*60*1000, // 5 mins time until lock auto expires - runWithLock: (path, runner, callback = ((error) ->)) -> - lockOpts = - wait: @MAX_LOCK_WAIT_TIME - pollPeriod: @LOCK_TEST_INTERVAL - stale: @LOCK_STALE - Lockfile.lock path, lockOpts, (error) -> - if error?.code is 'EEXIST' - return callback new Errors.AlreadyCompilingError("compile in progress") - else if error? - fs.lstat path, (statLockErr, statLock)-> - fs.lstat Path.dirname(path), (statDirErr, statDir)-> - fs.readdir Path.dirname(path), (readdirErr, readdirDir)-> - logger.err error:error, path:path, statLock:statLock, statLockErr:statLockErr, statDir:statDir, statDirErr: statDirErr, readdirErr:readdirErr, readdirDir:readdirDir, "unable to get lock" - return callback(error) - else - runner (error1, args...) -> - Lockfile.unlock path, (error2) -> - error = error1 or error2 - return callback(error) if error? - callback(null, args...) + runWithLock(path, runner, callback) { + if (callback == null) { callback = function(error) {}; } + const lockOpts = { + wait: this.MAX_LOCK_WAIT_TIME, + pollPeriod: this.LOCK_TEST_INTERVAL, + stale: this.LOCK_STALE + }; + return Lockfile.lock(path, lockOpts, function(error) { + if ((error != null ? error.code : undefined) === 'EEXIST') { + return callback(new Errors.AlreadyCompilingError("compile in progress")); + } else if (error != null) { + return fs.lstat(path, (statLockErr, statLock)=> + fs.lstat(Path.dirname(path), (statDirErr, statDir)=> + fs.readdir(Path.dirname(path), function(readdirErr, readdirDir){ + logger.err({error, path, statLock, statLockErr, statDir, statDirErr, readdirErr, readdirDir}, "unable to get lock"); + return callback(error); + }) + ) + ); + } else { + return runner((error1, ...args) => + Lockfile.unlock(path, function(error2) { + error = error1 || error2; + if (error != null) { return callback(error); } + return callback(null, ...Array.from(args)); + }) + ); + } + }); + } +}); diff --git a/app/coffee/Metrics.js b/app/coffee/Metrics.js index 9965b252..8148d664 100644 --- a/app/coffee/Metrics.js +++ b/app/coffee/Metrics.js @@ -1,2 +1,2 @@ -module.exports = require "metrics-sharelatex" +module.exports = require("metrics-sharelatex"); diff --git a/app/coffee/OutputCacheManager.js b/app/coffee/OutputCacheManager.js index 5ef92ec6..6d03a106 100644 --- a/app/coffee/OutputCacheManager.js +++ b/app/coffee/OutputCacheManager.js @@ -1,199 +1,270 @@ -async = require "async" -fs = require "fs" -fse = require "fs-extra" -Path = require "path" -logger = require "logger-sharelatex" -_ = require "underscore" -Settings = require "settings-sharelatex" -crypto = require "crypto" - -OutputFileOptimiser = require "./OutputFileOptimiser" - -module.exports = OutputCacheManager = - CACHE_SUBDIR: '.cache/clsi' - ARCHIVE_SUBDIR: '.archive/clsi' - # build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes - # for backwards compatibility, make the randombytes part optional - BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/ - CACHE_LIMIT: 2 # maximum number of cache directories - CACHE_AGE: 60*60*1000 # up to one hour old - - path: (buildId, file) -> - # used by static server, given build id return '.cache/clsi/buildId' - if buildId.match OutputCacheManager.BUILD_REGEX - return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file) - else - # for invalid build id, return top level - return file - - generateBuildId: (callback = (error, buildId) ->) -> - # generate a secure build id from Date.now() and 8 random bytes in hex - crypto.randomBytes 8, (err, buf) -> - return callback(err) if err? - random = buf.toString('hex') - date = Date.now().toString(16) - callback err, "#{date}-#{random}" - - saveOutputFiles: (outputFiles, compileDir, callback = (error) ->) -> - OutputCacheManager.generateBuildId (err, buildId) -> - return callback(err) if err? - OutputCacheManager.saveOutputFilesInBuildDir outputFiles, compileDir, buildId, callback - - saveOutputFilesInBuildDir: (outputFiles, compileDir, buildId, callback = (error) ->) -> - # make a compileDir/CACHE_SUBDIR/build_id directory and - # copy all the output files into it - cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR) - # Put the files into a new cache subdirectory - cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, 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 - OutputCacheManager.archiveLogs outputFiles, compileDir, buildId, (err) -> - if err? - logger.warn err:err, "erroring archiving log files" - - # make the new cache directory - fse.ensureDir cacheDir, (err) -> - if err? - logger.error err: err, directory: cacheDir, "error creating cache directory" - callback(err, outputFiles) - else - # copy all the output files into the new cache directory - results = [] - async.mapSeries outputFiles, (file, cb) -> - # don't send dot files as output, express doesn't serve them - if OutputCacheManager._fileIsHidden(file.path) - logger.debug compileDir: compileDir, path: file.path, "ignoring dotfile in output" - return cb() - # copy other files into cache directory if valid - newFile = _.clone(file) - [src, dst] = [Path.join(compileDir, file.path), Path.join(cacheDir, file.path)] - OutputCacheManager._checkFileIsSafe src, (err, isSafe) -> - return cb(err) if err? - if !isSafe - return cb() - OutputCacheManager._checkIfShouldCopy src, (err, shouldCopy) -> - return cb(err) if err? - if !shouldCopy - return cb() - OutputCacheManager._copyFile src, dst, (err) -> - return cb(err) if err? - newFile.build = buildId # attach a build id if we cached the file - results.push newFile - cb() - , (err) -> - 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: cacheDir, "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 1 else null} - - archiveLogs: (outputFiles, compileDir, buildId, callback = (error) ->) -> - archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId) - logger.log {dir: archiveDir}, "archiving log files for project" - fse.ensureDir archiveDir, (err) -> - return callback(err) if err? - async.mapSeries outputFiles, (file, cb) -> - [src, dst] = [Path.join(compileDir, file.path), Path.join(archiveDir, file.path)] - OutputCacheManager._checkFileIsSafe src, (err, isSafe) -> - return cb(err) if err? - return cb() if !isSafe - OutputCacheManager._checkIfShouldArchive src, (err, shouldArchive) -> - return cb(err) if err? - return cb() if !shouldArchive - OutputCacheManager._copyFile src, dst, cb - , callback - - expireOutputFiles: (cacheRoot, options, callback = (error) ->) -> - # look in compileDir for build dirs and delete if > N or age of mod time > T - fs.readdir cacheRoot, (err, results) -> - if err? - return callback(null) if err.code == 'ENOENT' # cache directory is empty - logger.error err: err, project_id: cacheRoot, "error clearing cache" - return callback(err) - - dirs = results.sort().reverse() - currentTime = Date.now() - - 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 - # DDDD is date and RRRR is random bytes - dirTime = parseInt(dir.split('-')?[0], 16) - age = currentTime - dirTime - return age > OutputCacheManager.CACHE_AGE - - toRemove = _.filter(dirs, isExpired) - - removeDir = (dir, cb) -> - fse.remove Path.join(cacheRoot, dir), (err, result) -> - logger.log cache: cacheRoot, dir: dir, "removed expired cache dir" - if err? - logger.error err: err, dir: dir, "cache remove error" - cb(err, result) - - async.eachSeries toRemove, (dir, cb) -> - removeDir dir, cb - , callback - - _fileIsHidden: (path) -> - return path?.match(/^\.|\/\./)? - - _checkFileIsSafe: (src, callback = (error, isSafe) ->) -> - # check if we have a valid file to copy into the cache - fs.stat src, (err, stats) -> - if err?.code is 'ENOENT' - logger.warn err: err, file: src, "file has disappeared before copying to build cache" - callback(err, false) - else if err? - # some other problem reading the file - logger.error err: err, file: src, "stat error for file in cache" - callback(err, false) - else if not stats.isFile() - # other filetype - reject it - logger.warn src: src, stat: stats, "nonfile output - refusing to copy to cache" - callback(null, false) - else - # it's a plain file, ok to copy - callback(null, true) - - _copyFile: (src, dst, callback) -> - # copy output file into the cache - fse.copy src, dst, (err) -> - if err?.code is 'ENOENT' - logger.warn err: err, file: src, "file has disappeared when copying to build cache" - callback(err, false) - else if err? - logger.error err: err, src: src, dst: dst, "copy error for file in cache" - callback(err) - else - if Settings.clsi?.optimiseInDocker - # don't run any optimisations on the pdf when they are done - # in the docker container - callback() - else - # call the optimiser for the file too - OutputFileOptimiser.optimiseFile src, dst, callback - - _checkIfShouldCopy: (src, callback = (err, shouldCopy) ->) -> - return callback(null, !Path.basename(src).match(/^strace/)) - - _checkIfShouldArchive: (src, callback = (err, shouldCopy) ->) -> - if Path.basename(src).match(/^strace/) - return callback(null, true) - if Settings.clsi?.archive_logs and Path.basename(src) in ["output.log", "output.blg"] - return callback(null, true) - return callback(null, false) +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let OutputCacheManager; +const async = require("async"); +const fs = require("fs"); +const fse = require("fs-extra"); +const Path = require("path"); +const logger = require("logger-sharelatex"); +const _ = require("underscore"); +const Settings = require("settings-sharelatex"); +const crypto = require("crypto"); + +const OutputFileOptimiser = require("./OutputFileOptimiser"); + +module.exports = (OutputCacheManager = { + CACHE_SUBDIR: '.cache/clsi', + ARCHIVE_SUBDIR: '.archive/clsi', + // build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes + // for backwards compatibility, make the randombytes part optional + BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/, + CACHE_LIMIT: 2, // maximum number of cache directories + CACHE_AGE: 60*60*1000, // up to one hour old + + path(buildId, file) { + // used by static server, given build id return '.cache/clsi/buildId' + if (buildId.match(OutputCacheManager.BUILD_REGEX)) { + return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file); + } else { + // for invalid build id, return top level + return file; + } + }, + + generateBuildId(callback) { + // generate a secure build id from Date.now() and 8 random bytes in hex + if (callback == null) { callback = function(error, buildId) {}; } + return crypto.randomBytes(8, function(err, buf) { + if (err != null) { return callback(err); } + const random = buf.toString('hex'); + const date = Date.now().toString(16); + return callback(err, `${date}-${random}`); + }); + }, + + saveOutputFiles(outputFiles, compileDir, callback) { + if (callback == null) { callback = function(error) {}; } + return OutputCacheManager.generateBuildId(function(err, buildId) { + if (err != null) { return callback(err); } + return OutputCacheManager.saveOutputFilesInBuildDir(outputFiles, compileDir, buildId, callback); + }); + }, + + saveOutputFilesInBuildDir(outputFiles, compileDir, buildId, callback) { + // make a compileDir/CACHE_SUBDIR/build_id directory and + // copy all the output files into it + if (callback == null) { callback = function(error) {}; } + const cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR); + // Put the files into a new cache subdirectory + const cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId); + // Is it a per-user compile? check if compile directory is PROJECTID-USERID + const perUser = Path.basename(compileDir).match(/^[0-9a-f]{24}-[0-9a-f]{24}$/); + + // Archive logs in background + if ((Settings.clsi != null ? Settings.clsi.archive_logs : undefined) || (Settings.clsi != null ? Settings.clsi.strace : undefined)) { + OutputCacheManager.archiveLogs(outputFiles, compileDir, buildId, function(err) { + if (err != null) { + return logger.warn({err}, "erroring archiving log files"); + } + }); + } + + // make the new cache directory + return fse.ensureDir(cacheDir, function(err) { + if (err != null) { + logger.error({err, directory: cacheDir}, "error creating cache directory"); + return callback(err, outputFiles); + } else { + // copy all the output files into the new cache directory + const results = []; + return async.mapSeries(outputFiles, function(file, cb) { + // don't send dot files as output, express doesn't serve them + if (OutputCacheManager._fileIsHidden(file.path)) { + logger.debug({compileDir, path: file.path}, "ignoring dotfile in output"); + return cb(); + } + // copy other files into cache directory if valid + const newFile = _.clone(file); + const [src, dst] = Array.from([Path.join(compileDir, file.path), Path.join(cacheDir, file.path)]); + return OutputCacheManager._checkFileIsSafe(src, function(err, isSafe) { + if (err != null) { return cb(err); } + if (!isSafe) { + return cb(); + } + return OutputCacheManager._checkIfShouldCopy(src, function(err, shouldCopy) { + if (err != null) { return cb(err); } + if (!shouldCopy) { + return cb(); + } + return OutputCacheManager._copyFile(src, dst, function(err) { + if (err != null) { return cb(err); } + newFile.build = buildId; // attach a build id if we cached the file + results.push(newFile); + return cb(); + }); + }); + }); + } + , function(err) { + if (err != null) { + // pass back the original files if we encountered *any* error + callback(err, outputFiles); + // clean up the directory we just created + return fse.remove(cacheDir, function(err) { + if (err != null) { + return logger.error({err, dir: cacheDir}, "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 + return OutputCacheManager.expireOutputFiles(cacheRoot, {keep: buildId, limit: perUser ? 1 : null}); + } + }); + } + }); + }, + + archiveLogs(outputFiles, compileDir, buildId, callback) { + if (callback == null) { callback = function(error) {}; } + const archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId); + logger.log({dir: archiveDir}, "archiving log files for project"); + return fse.ensureDir(archiveDir, function(err) { + if (err != null) { return callback(err); } + return async.mapSeries(outputFiles, function(file, cb) { + const [src, dst] = Array.from([Path.join(compileDir, file.path), Path.join(archiveDir, file.path)]); + return OutputCacheManager._checkFileIsSafe(src, function(err, isSafe) { + if (err != null) { return cb(err); } + if (!isSafe) { return cb(); } + return OutputCacheManager._checkIfShouldArchive(src, function(err, shouldArchive) { + if (err != null) { return cb(err); } + if (!shouldArchive) { return cb(); } + return OutputCacheManager._copyFile(src, dst, cb); + }); + }); + } + , callback); + }); + }, + + expireOutputFiles(cacheRoot, options, callback) { + // look in compileDir for build dirs and delete if > N or age of mod time > T + if (callback == null) { callback = function(error) {}; } + return fs.readdir(cacheRoot, function(err, results) { + if (err != null) { + if (err.code === 'ENOENT') { return callback(null); } // cache directory is empty + logger.error({err, project_id: cacheRoot}, "error clearing cache"); + return callback(err); + } + + const dirs = results.sort().reverse(); + const currentTime = Date.now(); + + const isExpired = function(dir, index) { + if ((options != null ? options.keep : undefined) === dir) { return false; } + // remove any directories over the requested (non-null) limit + if (((options != null ? options.limit : undefined) != null) && (index > options.limit)) { return true; } + // remove any directories over the hard limit + if (index > OutputCacheManager.CACHE_LIMIT) { return true; } + // we can get the build time from the first part of the directory name DDDD-RRRR + // DDDD is date and RRRR is random bytes + const dirTime = parseInt(__guard__(dir.split('-'), x => x[0]), 16); + const age = currentTime - dirTime; + return age > OutputCacheManager.CACHE_AGE; + }; + + const toRemove = _.filter(dirs, isExpired); + + const removeDir = (dir, cb) => + fse.remove(Path.join(cacheRoot, dir), function(err, result) { + logger.log({cache: cacheRoot, dir}, "removed expired cache dir"); + if (err != null) { + logger.error({err, dir}, "cache remove error"); + } + return cb(err, result); + }) + ; + + return async.eachSeries(toRemove, (dir, cb) => removeDir(dir, cb) + , callback); + }); + }, + + _fileIsHidden(path) { + return ((path != null ? path.match(/^\.|\/\./) : undefined) != null); + }, + + _checkFileIsSafe(src, callback) { + // check if we have a valid file to copy into the cache + if (callback == null) { callback = function(error, isSafe) {}; } + return fs.stat(src, function(err, stats) { + if ((err != null ? err.code : undefined) === 'ENOENT') { + logger.warn({err, file: src}, "file has disappeared before copying to build cache"); + return callback(err, false); + } else if (err != null) { + // some other problem reading the file + logger.error({err, file: src}, "stat error for file in cache"); + return callback(err, false); + } else if (!stats.isFile()) { + // other filetype - reject it + logger.warn({src, stat: stats}, "nonfile output - refusing to copy to cache"); + return callback(null, false); + } else { + // it's a plain file, ok to copy + return callback(null, true); + } + }); + }, + + _copyFile(src, dst, callback) { + // copy output file into the cache + return fse.copy(src, dst, function(err) { + if ((err != null ? err.code : undefined) === 'ENOENT') { + logger.warn({err, file: src}, "file has disappeared when copying to build cache"); + return callback(err, false); + } else if (err != null) { + logger.error({err, src, dst}, "copy error for file in cache"); + return callback(err); + } else { + if ((Settings.clsi != null ? Settings.clsi.optimiseInDocker : undefined)) { + // don't run any optimisations on the pdf when they are done + // in the docker container + return callback(); + } else { + // call the optimiser for the file too + return OutputFileOptimiser.optimiseFile(src, dst, callback); + } + } + }); + }, + + _checkIfShouldCopy(src, callback) { + if (callback == null) { callback = function(err, shouldCopy) {}; } + return callback(null, !Path.basename(src).match(/^strace/)); + }, + + _checkIfShouldArchive(src, callback) { + let needle; + if (callback == null) { callback = function(err, shouldCopy) {}; } + if (Path.basename(src).match(/^strace/)) { + return callback(null, true); + } + if ((Settings.clsi != null ? Settings.clsi.archive_logs : undefined) && (needle = Path.basename(src), ["output.log", "output.blg"].includes(needle))) { + return callback(null, true); + } + return callback(null, false); + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/app/coffee/OutputFileFinder.js b/app/coffee/OutputFileFinder.js index 662440b5..f0f837c0 100644 --- a/app/coffee/OutputFileFinder.js +++ b/app/coffee/OutputFileFinder.js @@ -1,50 +1,78 @@ -async = require "async" -fs = require "fs" -Path = require "path" -spawn = require("child_process").spawn -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let OutputFileFinder; +const async = require("async"); +const fs = require("fs"); +const Path = require("path"); +const { spawn } = require("child_process"); +const logger = require("logger-sharelatex"); -module.exports = OutputFileFinder = - findOutputFiles: (resources, directory, callback = (error, outputFiles, allFiles) ->) -> - incomingResources = {} - for resource in resources - incomingResources[resource.path] = true +module.exports = (OutputFileFinder = { + findOutputFiles(resources, directory, callback) { + if (callback == null) { callback = function(error, outputFiles, allFiles) {}; } + const incomingResources = {}; + for (let resource of Array.from(resources)) { + incomingResources[resource.path] = true; + } - OutputFileFinder._getAllFiles directory, (error, allFiles = []) -> - if error? - logger.err err:error, "error finding all output files" - return callback(error) - outputFiles = [] - for file in allFiles - if !incomingResources[file] - outputFiles.push { - path: file - type: file.match(/\.([^\.]+)$/)?[1] - } - callback null, outputFiles, allFiles + return OutputFileFinder._getAllFiles(directory, function(error, allFiles) { + if (allFiles == null) { allFiles = []; } + if (error != null) { + logger.err({err:error}, "error finding all output files"); + return callback(error); + } + const outputFiles = []; + for (let file of Array.from(allFiles)) { + if (!incomingResources[file]) { + outputFiles.push({ + path: file, + type: __guard__(file.match(/\.([^\.]+)$/), x => x[1]) + }); + } + } + return callback(null, outputFiles, allFiles); + }); + }, - _getAllFiles: (directory, _callback = (error, fileList) ->) -> - callback = (error, fileList) -> - _callback(error, fileList) - _callback = () -> + _getAllFiles(directory, _callback) { + if (_callback == null) { _callback = function(error, fileList) {}; } + const callback = function(error, fileList) { + _callback(error, fileList); + return _callback = function() {}; + }; - # don't include clsi-specific files/directories in the output list - EXCLUDE_DIRS = ["-name", ".cache", "-o", "-name", ".archive","-o", "-name", ".project-*"] - args = [directory, "(", EXCLUDE_DIRS..., ")", "-prune", "-o", "-type", "f", "-print"] - logger.log args: args, "running find command" + // don't include clsi-specific files/directories in the output list + const EXCLUDE_DIRS = ["-name", ".cache", "-o", "-name", ".archive","-o", "-name", ".project-*"]; + const args = [directory, "(", ...Array.from(EXCLUDE_DIRS), ")", "-prune", "-o", "-type", "f", "-print"]; + logger.log({args}, "running find command"); - proc = spawn("find", args) - stdout = "" - proc.stdout.on "data", (chunk) -> - stdout += chunk.toString() - proc.on "error", callback - proc.on "close", (code) -> - if code != 0 - logger.warn {directory, code}, "find returned error, directory likely doesn't exist" - return callback null, [] - fileList = stdout.trim().split("\n") - fileList = fileList.map (file) -> - # Strip leading directory - path = Path.relative(directory, file) - return callback null, fileList + const proc = spawn("find", args); + let stdout = ""; + proc.stdout.on("data", chunk => stdout += chunk.toString()); + proc.on("error", callback); + return proc.on("close", function(code) { + if (code !== 0) { + logger.warn({directory, code}, "find returned error, directory likely doesn't exist"); + return callback(null, []); + } + let fileList = stdout.trim().split("\n"); + fileList = fileList.map(function(file) { + // Strip leading directory + let path; + return path = Path.relative(directory, file); + }); + return callback(null, fileList); + }); + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/app/coffee/OutputFileOptimiser.js b/app/coffee/OutputFileOptimiser.js index b702f36f..f8302aac 100644 --- a/app/coffee/OutputFileOptimiser.js +++ b/app/coffee/OutputFileOptimiser.js @@ -1,55 +1,77 @@ -fs = require "fs" -Path = require "path" -spawn = require("child_process").spawn -logger = require "logger-sharelatex" -Metrics = require "./Metrics" -_ = require "underscore" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let OutputFileOptimiser; +const fs = require("fs"); +const Path = require("path"); +const { spawn } = require("child_process"); +const logger = require("logger-sharelatex"); +const Metrics = require("./Metrics"); +const _ = require("underscore"); -module.exports = OutputFileOptimiser = +module.exports = (OutputFileOptimiser = { - optimiseFile: (src, dst, callback = (error) ->) -> - # check output file (src) and see if we can optimise it, storing - # the result in the build directory (dst) - if src.match(/\/output\.pdf$/) - OutputFileOptimiser.checkIfPDFIsOptimised src, (err, isOptimised) -> - return callback(null) if err? or isOptimised - OutputFileOptimiser.optimisePDF src, dst, callback - else - callback (null) + optimiseFile(src, dst, callback) { + // check output file (src) and see if we can optimise it, storing + // the result in the build directory (dst) + if (callback == null) { callback = function(error) {}; } + if (src.match(/\/output\.pdf$/)) { + return OutputFileOptimiser.checkIfPDFIsOptimised(src, function(err, isOptimised) { + if ((err != null) || isOptimised) { return callback(null); } + return OutputFileOptimiser.optimisePDF(src, dst, callback); + }); + } else { + return callback((null)); + } + }, - checkIfPDFIsOptimised: (file, callback) -> - SIZE = 16*1024 # check the header of the pdf - result = new Buffer(SIZE) - result.fill(0) # prevent leakage of uninitialised buffer - fs.open file, "r", (err, fd) -> - return callback(err) if err? - fs.read fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) -> - fs.close fd, (errClose) -> - return callback(errRead) if errRead? - return callback(errClose) if errReadClose? - isOptimised = buffer.toString('ascii').indexOf("/Linearized 1") >= 0 - callback(null, isOptimised) + checkIfPDFIsOptimised(file, callback) { + const SIZE = 16*1024; // check the header of the pdf + const result = new Buffer(SIZE); + result.fill(0); // prevent leakage of uninitialised buffer + return fs.open(file, "r", function(err, fd) { + if (err != null) { return callback(err); } + return fs.read(fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) => + fs.close(fd, function(errClose) { + if (errRead != null) { return callback(errRead); } + if (typeof errReadClose !== 'undefined' && errReadClose !== null) { return callback(errClose); } + const isOptimised = buffer.toString('ascii').indexOf("/Linearized 1") >= 0; + return callback(null, isOptimised); + }) + ); + }); + }, - optimisePDF: (src, dst, callback = (error) ->) -> - tmpOutput = dst + '.opt' - args = ["--linearize", src, tmpOutput] - logger.log args: args, "running qpdf command" + optimisePDF(src, dst, callback) { + if (callback == null) { callback = function(error) {}; } + const tmpOutput = dst + '.opt'; + const args = ["--linearize", src, tmpOutput]; + logger.log({args}, "running qpdf command"); - timer = new Metrics.Timer("qpdf") - proc = spawn("qpdf", args) - stdout = "" - proc.stdout.on "data", (chunk) -> - stdout += chunk.toString() - callback = _.once(callback) # avoid double call back for error and close event - proc.on "error", (err) -> - logger.warn {err, args}, "qpdf failed" - callback(null) # ignore the error - proc.on "close", (code) -> - timer.done() - if code != 0 - logger.warn {code, args}, "qpdf returned error" - return callback(null) # ignore the error - fs.rename tmpOutput, dst, (err) -> - if err? - logger.warn {tmpOutput, dst}, "failed to rename output of qpdf command" - callback(null) # ignore the error + const timer = new Metrics.Timer("qpdf"); + const proc = spawn("qpdf", args); + let stdout = ""; + proc.stdout.on("data", chunk => stdout += chunk.toString()); + callback = _.once(callback); // avoid double call back for error and close event + proc.on("error", function(err) { + logger.warn({err, args}, "qpdf failed"); + return callback(null); + }); // ignore the error + return proc.on("close", function(code) { + timer.done(); + if (code !== 0) { + logger.warn({code, args}, "qpdf returned error"); + return callback(null); // ignore the error + } + return fs.rename(tmpOutput, dst, function(err) { + if (err != null) { + logger.warn({tmpOutput, dst}, "failed to rename output of qpdf command"); + } + return callback(null); + }); + }); + } // ignore the error +}); diff --git a/app/coffee/ProjectPersistenceManager.js b/app/coffee/ProjectPersistenceManager.js index 4ea02bf7..7b3d5ee2 100644 --- a/app/coffee/ProjectPersistenceManager.js +++ b/app/coffee/ProjectPersistenceManager.js @@ -1,84 +1,117 @@ -UrlCache = require "./UrlCache" -CompileManager = require "./CompileManager" -db = require "./db" -dbQueue = require "./DbQueue" -async = require "async" -logger = require "logger-sharelatex" -oneDay = 24 * 60 * 60 * 1000 -Settings = require "settings-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectPersistenceManager; +const UrlCache = require("./UrlCache"); +const CompileManager = require("./CompileManager"); +const db = require("./db"); +const dbQueue = require("./DbQueue"); +const async = require("async"); +const logger = require("logger-sharelatex"); +const oneDay = 24 * 60 * 60 * 1000; +const Settings = require("settings-sharelatex"); -module.exports = ProjectPersistenceManager = +module.exports = (ProjectPersistenceManager = { - EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5 + EXPIRY_TIMEOUT: Settings.project_cache_length_ms || (oneDay * 2.5), - markProjectAsJustAccessed: (project_id, callback = (error) ->) -> - job = (cb)-> - db.Project.findOrCreate(where: {project_id: project_id}) + markProjectAsJustAccessed(project_id, callback) { + if (callback == null) { callback = function(error) {}; } + const job = cb=> + db.Project.findOrCreate({where: {project_id}}) .spread( - (project, created) -> - project.updateAttributes(lastAccessed: new Date()) - .then(() -> cb()) - .error cb + (project, created) => + project.updateAttributes({lastAccessed: new Date()}) + .then(() => cb()) + .error(cb) ) - .error cb - dbQueue.queue.push(job, callback) + .error(cb) + ; + return dbQueue.queue.push(job, callback); + }, - clearExpiredProjects: (callback = (error) ->) -> - ProjectPersistenceManager._findExpiredProjectIds (error, project_ids) -> - return callback(error) if error? - logger.log project_ids: project_ids, "clearing expired projects" - jobs = for project_id in (project_ids or []) - do (project_id) -> - (callback) -> - ProjectPersistenceManager.clearProjectFromCache project_id, (err) -> - if err? - logger.error err: err, project_id: project_id, "error clearing project" - callback() - async.series jobs, (error) -> - return callback(error) if error? - CompileManager.clearExpiredProjects ProjectPersistenceManager.EXPIRY_TIMEOUT, (error) -> - callback() # ignore any errors from deleting directories + clearExpiredProjects(callback) { + if (callback == null) { callback = function(error) {}; } + return ProjectPersistenceManager._findExpiredProjectIds(function(error, project_ids) { + if (error != null) { return callback(error); } + logger.log({project_ids}, "clearing expired projects"); + const jobs = (Array.from(project_ids || [])).map((project_id) => + (project_id => + callback => + ProjectPersistenceManager.clearProjectFromCache(project_id, function(err) { + if (err != null) { + logger.error({err, project_id}, "error clearing project"); + } + return callback(); + }) + + )(project_id)); + return async.series(jobs, function(error) { + if (error != null) { return callback(error); } + return CompileManager.clearExpiredProjects(ProjectPersistenceManager.EXPIRY_TIMEOUT, error => callback()); + }); + }); + }, // ignore any errors from deleting directories - clearProject: (project_id, user_id, callback = (error) ->) -> - logger.log project_id: project_id, user_id:user_id, "clearing project for user" - CompileManager.clearProject project_id, user_id, (error) -> - return callback(error) if error? - ProjectPersistenceManager.clearProjectFromCache project_id, (error) -> - return callback(error) if error? - callback() + clearProject(project_id, user_id, callback) { + if (callback == null) { callback = function(error) {}; } + logger.log({project_id, user_id}, "clearing project for user"); + return CompileManager.clearProject(project_id, user_id, function(error) { + if (error != null) { return callback(error); } + return ProjectPersistenceManager.clearProjectFromCache(project_id, function(error) { + if (error != null) { return callback(error); } + return callback(); + }); + }); + }, - clearProjectFromCache: (project_id, callback = (error) ->) -> - logger.log project_id: project_id, "clearing project from cache" - UrlCache.clearProject project_id, (error) -> - if error? - logger.err error:error, project_id: project_id, "error clearing project from cache" - return callback(error) - ProjectPersistenceManager._clearProjectFromDatabase project_id, (error) -> - if error? - logger.err error:error, project_id:project_id, "error clearing project from database" - callback(error) + clearProjectFromCache(project_id, callback) { + if (callback == null) { callback = function(error) {}; } + logger.log({project_id}, "clearing project from cache"); + return UrlCache.clearProject(project_id, function(error) { + if (error != null) { + logger.err({error, project_id}, "error clearing project from cache"); + return callback(error); + } + return ProjectPersistenceManager._clearProjectFromDatabase(project_id, function(error) { + if (error != null) { + logger.err({error, project_id}, "error clearing project from database"); + } + return callback(error); + }); + }); + }, - _clearProjectFromDatabase: (project_id, callback = (error) ->) -> - logger.log project_id:project_id, "clearing project from database" - job = (cb)-> - db.Project.destroy(where: {project_id: project_id}) - .then(() -> cb()) - .error cb - dbQueue.queue.push(job, callback) + _clearProjectFromDatabase(project_id, callback) { + if (callback == null) { callback = function(error) {}; } + logger.log({project_id}, "clearing project from database"); + const job = cb=> + db.Project.destroy({where: {project_id}}) + .then(() => cb()) + .error(cb) + ; + return dbQueue.queue.push(job, callback); + }, - _findExpiredProjectIds: (callback = (error, project_ids) ->) -> - job = (cb)-> - keepProjectsFrom = new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT) - q = {} - q[db.op.lt] = keepProjectsFrom - db.Project.findAll(where:{lastAccessed:q}) - .then((projects) -> - cb null, projects.map((project) -> project.project_id) - ).error cb + _findExpiredProjectIds(callback) { + if (callback == null) { callback = function(error, project_ids) {}; } + const job = function(cb){ + const keepProjectsFrom = new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT); + const q = {}; + q[db.op.lt] = keepProjectsFrom; + return db.Project.findAll({where:{lastAccessed:q}}) + .then(projects => cb(null, projects.map(project => project.project_id))).error(cb); + }; - dbQueue.queue.push(job, callback) + return dbQueue.queue.push(job, callback); + } +}); -logger.log {EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout" +logger.log({EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout"); diff --git a/app/coffee/RequestParser.js b/app/coffee/RequestParser.js index 9b947124..fdfb8bf4 100644 --- a/app/coffee/RequestParser.js +++ b/app/coffee/RequestParser.js @@ -1,128 +1,182 @@ -settings = require("settings-sharelatex") - -module.exports = RequestParser = - VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"] - MAX_TIMEOUT: 600 - - parse: (body, callback = (error, data) ->) -> - response = {} - - if !body.compile? - return callback "top level object should have a compile attribute" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RequestParser; +const settings = require("settings-sharelatex"); + +module.exports = (RequestParser = { + VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"], + MAX_TIMEOUT: 600, + + parse(body, callback) { + let resource; + if (callback == null) { callback = function(error, data) {}; } + const response = {}; + + if ((body.compile == null)) { + return callback("top level object should have a compile attribute"); + } - compile = body.compile - compile.options ||= {} + const { compile } = body; + if (!compile.options) { compile.options = {}; } - try - response.compiler = @_parseAttribute "compiler", - compile.options.compiler, - validValues: @VALID_COMPILERS - default: "pdflatex" + try { + response.compiler = this._parseAttribute("compiler", + compile.options.compiler, { + validValues: this.VALID_COMPILERS, + default: "pdflatex", type: "string" - response.timeout = @_parseAttribute "timeout", - compile.options.timeout - default: RequestParser.MAX_TIMEOUT + } + ); + response.timeout = this._parseAttribute("timeout", + compile.options.timeout, { + default: RequestParser.MAX_TIMEOUT, type: "number" - response.imageName = @_parseAttribute "imageName", + } + ); + response.imageName = this._parseAttribute("imageName", compile.options.imageName, - type: "string" - response.draft = @_parseAttribute "draft", - compile.options.draft, + {type: "string"}); + response.draft = this._parseAttribute("draft", + compile.options.draft, { default: false, type: "boolean" - response.check = @_parseAttribute "check", + } + ); + response.check = this._parseAttribute("check", compile.options.check, - type: "string" - response.flags = @_parseAttribute "flags", - compile.options.flags, + {type: "string"}); + response.flags = this._parseAttribute("flags", + compile.options.flags, { default: [], type: "object" - - # The syncType specifies whether the request contains all - # resources (full) or only those resources to be updated - # in-place (incremental). - response.syncType = @_parseAttribute "syncType", - compile.options.syncType, - validValues: ["full", "incremental"] + } + ); + + // The syncType specifies whether the request contains all + // resources (full) or only those resources to be updated + // in-place (incremental). + response.syncType = this._parseAttribute("syncType", + compile.options.syncType, { + validValues: ["full", "incremental"], type: "string" - - # The syncState is an identifier passed in with the request - # which has the property that it changes when any resource is - # added, deleted, moved or renamed. - # - # on syncType full the syncState identifier is passed in and - # stored - # - # on syncType incremental the syncState identifier must match - # the stored value - response.syncState = @_parseAttribute "syncState", + } + ); + + // The syncState is an identifier passed in with the request + // which has the property that it changes when any resource is + // added, deleted, moved or renamed. + // + // on syncType full the syncState identifier is passed in and + // stored + // + // on syncType incremental the syncState identifier must match + // the stored value + response.syncState = this._parseAttribute("syncState", compile.options.syncState, + {type: "string"}); + + if (response.timeout > RequestParser.MAX_TIMEOUT) { + response.timeout = RequestParser.MAX_TIMEOUT; + } + response.timeout = response.timeout * 1000; // milliseconds + + response.resources = ((() => { + const result = []; + for (resource of Array.from((compile.resources || []))) { result.push(this._parseResource(resource)); + } + return result; + })()); + + const rootResourcePath = this._parseAttribute("rootResourcePath", + compile.rootResourcePath, { + default: "main.tex", type: "string" + } + ); + const originalRootResourcePath = rootResourcePath; + const sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath); + response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath); + + for (resource of Array.from(response.resources)) { + if (resource.path === originalRootResourcePath) { + resource.path = sanitizedRootResourcePath; + } + } + } catch (error1) { + const error = error1; + return callback(error); + } - if response.timeout > RequestParser.MAX_TIMEOUT - response.timeout = RequestParser.MAX_TIMEOUT - response.timeout = response.timeout * 1000 # milliseconds + return callback(null, response); + }, - response.resources = (@_parseResource(resource) for resource in (compile.resources or [])) + _parseResource(resource) { + let modified; + if ((resource.path == null) || (typeof resource.path !== "string")) { + throw "all resources should have a path attribute"; + } - rootResourcePath = @_parseAttribute "rootResourcePath", - compile.rootResourcePath - default: "main.tex" - type: "string" - originalRootResourcePath = rootResourcePath - sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath) - response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath) - - for resource in response.resources - if resource.path == originalRootResourcePath - resource.path = sanitizedRootResourcePath - catch error - return callback error - - callback null, response - - _parseResource: (resource) -> - if !resource.path? or typeof resource.path != "string" - throw "all resources should have a path attribute" - - if resource.modified? - modified = new Date(resource.modified) - if isNaN(modified.getTime()) - throw "resource modified date could not be understood: #{resource.modified}" - - if !resource.url? and !resource.content? - throw "all resources should have either a url or content attribute" - if resource.content? and typeof resource.content != "string" - throw "content attribute should be a string" - if resource.url? and typeof resource.url != "string" - throw "url attribute should be a string" + if (resource.modified != null) { + modified = new Date(resource.modified); + if (isNaN(modified.getTime())) { + throw `resource modified date could not be understood: ${resource.modified}`; + } + } + + if ((resource.url == null) && (resource.content == null)) { + throw "all resources should have either a url or content attribute"; + } + if ((resource.content != null) && (typeof resource.content !== "string")) { + throw "content attribute should be a string"; + } + if ((resource.url != null) && (typeof resource.url !== "string")) { + throw "url attribute should be a string"; + } return { - path: resource.path - modified: modified - url: resource.url + path: resource.path, + modified, + url: resource.url, content: resource.content + }; + }, + + _parseAttribute(name, attribute, options) { + if (attribute != null) { + if (options.validValues != null) { + if (options.validValues.indexOf(attribute) === -1) { + throw `${name} attribute should be one of: ${options.validValues.join(", ")}`; + } + } + if (options.type != null) { + if (typeof attribute !== options.type) { + throw `${name} attribute should be a ${options.type}`; + } + } + } else { + if (options.default != null) { return options.default; } } - - _parseAttribute: (name, attribute, options) -> - if attribute? - if options.validValues? - if options.validValues.indexOf(attribute) == -1 - throw "#{name} attribute should be one of: #{options.validValues.join(", ")}" - if options.type? - if typeof attribute != options.type - throw "#{name} attribute should be a #{options.type}" - else - return options.default if options.default? - return attribute - - _sanitizePath: (path) -> - # See http://php.net/manual/en/function.escapeshellcmd.php - path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, "") - - _checkPath: (path) -> - # check that the request does not use a relative path - for dir in path.split('/') - if dir == '..' - throw "relative path in root resource" - return path + return attribute; + }, + + _sanitizePath(path) { + // See http://php.net/manual/en/function.escapeshellcmd.php + return path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, ""); + }, + + _checkPath(path) { + // check that the request does not use a relative path + for (let dir of Array.from(path.split('/'))) { + if (dir === '..') { + throw "relative path in root resource"; + } + } + return path; + } +}); diff --git a/app/coffee/ResourceStateManager.js b/app/coffee/ResourceStateManager.js index 19fea47d..f430c8fd 100644 --- a/app/coffee/ResourceStateManager.js +++ b/app/coffee/ResourceStateManager.js @@ -1,72 +1,108 @@ -Path = require "path" -fs = require "fs" -logger = require "logger-sharelatex" -settings = require("settings-sharelatex") -Errors = require "./Errors" -SafeReader = require "./SafeReader" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ResourceStateManager; +const Path = require("path"); +const fs = require("fs"); +const logger = require("logger-sharelatex"); +const settings = require("settings-sharelatex"); +const Errors = require("./Errors"); +const SafeReader = require("./SafeReader"); -module.exports = ResourceStateManager = +module.exports = (ResourceStateManager = { - # The sync state is an identifier which must match for an - # incremental update to be allowed. - # - # The initial value is passed in and stored on a full - # compile, along with the list of resources.. - # - # Subsequent incremental compiles must come with the same value - if - # not they will be rejected with a 409 Conflict response. The - # previous list of resources is returned. - # - # An incremental compile can only update existing files with new - # content. The sync state identifier must change if any docs or - # files are moved, added, deleted or renamed. + // The sync state is an identifier which must match for an + // incremental update to be allowed. + // + // The initial value is passed in and stored on a full + // compile, along with the list of resources.. + // + // Subsequent incremental compiles must come with the same value - if + // not they will be rejected with a 409 Conflict response. The + // previous list of resources is returned. + // + // An incremental compile can only update existing files with new + // content. The sync state identifier must change if any docs or + // files are moved, added, deleted or renamed. - SYNC_STATE_FILE: ".project-sync-state" - SYNC_STATE_MAX_SIZE: 128*1024 + SYNC_STATE_FILE: ".project-sync-state", + SYNC_STATE_MAX_SIZE: 128*1024, - saveProjectState: (state, resources, basePath, callback = (error) ->) -> - stateFile = Path.join(basePath, @SYNC_STATE_FILE) - if not state? # remove the file if no state passed in - logger.log state:state, basePath:basePath, "clearing sync state" - fs.unlink stateFile, (err) -> - if err? and err.code isnt 'ENOENT' - return callback(err) - else - return callback() - else - logger.log state:state, basePath:basePath, "writing sync state" - resourceList = (resource.path for resource in resources) - fs.writeFile stateFile, [resourceList..., "stateHash:#{state}"].join("\n"), callback + saveProjectState(state, resources, basePath, callback) { + if (callback == null) { callback = function(error) {}; } + const stateFile = Path.join(basePath, this.SYNC_STATE_FILE); + if ((state == null)) { // remove the file if no state passed in + logger.log({state, basePath}, "clearing sync state"); + return fs.unlink(stateFile, function(err) { + if ((err != null) && (err.code !== 'ENOENT')) { + return callback(err); + } else { + return callback(); + } + }); + } else { + logger.log({state, basePath}, "writing sync state"); + const resourceList = (Array.from(resources).map((resource) => resource.path)); + return fs.writeFile(stateFile, [...Array.from(resourceList), `stateHash:${state}`].join("\n"), callback); + } + }, - checkProjectStateMatches: (state, basePath, callback = (error, resources) ->) -> - stateFile = Path.join(basePath, @SYNC_STATE_FILE) - size = @SYNC_STATE_MAX_SIZE - SafeReader.readFile stateFile, size, 'utf8', (err, result, bytesRead) -> - return callback(err) if err? - if bytesRead is size - logger.error file:stateFile, size:size, bytesRead:bytesRead, "project state file truncated" - [resourceList..., oldState] = result?.toString()?.split("\n") or [] - newState = "stateHash:#{state}" - logger.log state:state, oldState: oldState, basePath:basePath, stateMatches: (newState is oldState), "checking sync state" - if newState isnt oldState - return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") - else - resources = ({path: path} for path in resourceList) - callback(null, resources) + checkProjectStateMatches(state, basePath, callback) { + if (callback == null) { callback = function(error, resources) {}; } + const stateFile = Path.join(basePath, this.SYNC_STATE_FILE); + const size = this.SYNC_STATE_MAX_SIZE; + return SafeReader.readFile(stateFile, size, 'utf8', function(err, result, bytesRead) { + if (err != null) { return callback(err); } + if (bytesRead === size) { + logger.error({file:stateFile, size, bytesRead}, "project state file truncated"); + } + const array = __guard__(result != null ? result.toString() : undefined, x => x.split("\n")) || [], + adjustedLength = Math.max(array.length, 1), + resourceList = array.slice(0, adjustedLength - 1), + oldState = array[adjustedLength - 1]; + const newState = `stateHash:${state}`; + logger.log({state, oldState, basePath, stateMatches: (newState === oldState)}, "checking sync state"); + if (newState !== oldState) { + return callback(new Errors.FilesOutOfSyncError("invalid state for incremental update")); + } else { + const resources = (Array.from(resourceList).map((path) => ({path}))); + return callback(null, resources); + } + }); + }, - checkResourceFiles: (resources, allFiles, basePath, callback = (error) ->) -> - # check the paths are all relative to current directory - for file in resources or [] - for dir in file?.path?.split('/') - if dir == '..' - return callback new Error("relative path in resource file list") - # check if any of the input files are not present in list of files - seenFile = {} - for file in allFiles - seenFile[file] = true - missingFiles = (resource.path for resource in resources when not seenFile[resource.path]) - if missingFiles?.length > 0 - logger.err missingFiles:missingFiles, basePath:basePath, allFiles:allFiles, resources:resources, "missing input files for project" - return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update") - else - callback() + checkResourceFiles(resources, allFiles, basePath, callback) { + // check the paths are all relative to current directory + let file; + if (callback == null) { callback = function(error) {}; } + for (file of Array.from(resources || [])) { + for (let dir of Array.from(__guard__(file != null ? file.path : undefined, x => x.split('/')))) { + if (dir === '..') { + return callback(new Error("relative path in resource file list")); + } + } + } + // check if any of the input files are not present in list of files + const seenFile = {}; + for (file of Array.from(allFiles)) { + seenFile[file] = true; + } + const missingFiles = (Array.from(resources).filter((resource) => !seenFile[resource.path]).map((resource) => resource.path)); + if ((missingFiles != null ? missingFiles.length : undefined) > 0) { + logger.err({missingFiles, basePath, allFiles, resources}, "missing input files for project"); + return callback(new Errors.FilesOutOfSyncError("resource files missing in incremental update")); + } else { + return callback(); + } + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/app/coffee/ResourceWriter.js b/app/coffee/ResourceWriter.js index f3a4bd07..0044ad93 100644 --- a/app/coffee/ResourceWriter.js +++ b/app/coffee/ResourceWriter.js @@ -1,142 +1,206 @@ -UrlCache = require "./UrlCache" -Path = require "path" -fs = require "fs" -async = require "async" -mkdirp = require "mkdirp" -OutputFileFinder = require "./OutputFileFinder" -ResourceStateManager = require "./ResourceStateManager" -Metrics = require "./Metrics" -logger = require "logger-sharelatex" -settings = require("settings-sharelatex") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ResourceWriter; +const UrlCache = require("./UrlCache"); +const Path = require("path"); +const fs = require("fs"); +const async = require("async"); +const mkdirp = require("mkdirp"); +const OutputFileFinder = require("./OutputFileFinder"); +const ResourceStateManager = require("./ResourceStateManager"); +const Metrics = require("./Metrics"); +const logger = require("logger-sharelatex"); +const settings = require("settings-sharelatex"); -parallelFileDownloads = settings.parallelFileDownloads or 1 +const parallelFileDownloads = settings.parallelFileDownloads || 1; -module.exports = ResourceWriter = +module.exports = (ResourceWriter = { - syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) -> - if request.syncType is "incremental" - logger.log project_id: request.project_id, user_id: request.user_id, "incremental sync" - ResourceStateManager.checkProjectStateMatches request.syncState, basePath, (error, resourceList) -> - return callback(error) if error? - ResourceWriter._removeExtraneousFiles resourceList, basePath, (error, outputFiles, allFiles) -> - return callback(error) if error? - ResourceStateManager.checkResourceFiles resourceList, allFiles, basePath, (error) -> - return callback(error) if error? - ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> - return callback(error) if error? - callback(null, resourceList) - else - logger.log project_id: request.project_id, user_id: request.user_id, "full sync" - @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> - return callback(error) if error? - ResourceStateManager.saveProjectState request.syncState, request.resources, basePath, (error) -> - return callback(error) if error? - callback(null, request.resources) + syncResourcesToDisk(request, basePath, callback) { + if (callback == null) { callback = function(error, resourceList) {}; } + if (request.syncType === "incremental") { + logger.log({project_id: request.project_id, user_id: request.user_id}, "incremental sync"); + return ResourceStateManager.checkProjectStateMatches(request.syncState, basePath, function(error, resourceList) { + if (error != null) { return callback(error); } + return ResourceWriter._removeExtraneousFiles(resourceList, basePath, function(error, outputFiles, allFiles) { + if (error != null) { return callback(error); } + return ResourceStateManager.checkResourceFiles(resourceList, allFiles, basePath, function(error) { + if (error != null) { return callback(error); } + return ResourceWriter.saveIncrementalResourcesToDisk(request.project_id, request.resources, basePath, function(error) { + if (error != null) { return callback(error); } + return callback(null, resourceList); + }); + }); + }); + }); + } else { + logger.log({project_id: request.project_id, user_id: request.user_id}, "full sync"); + return this.saveAllResourcesToDisk(request.project_id, request.resources, basePath, function(error) { + if (error != null) { return callback(error); } + return ResourceStateManager.saveProjectState(request.syncState, request.resources, basePath, function(error) { + if (error != null) { return callback(error); } + return callback(null, request.resources); + }); + }); + } + }, - saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> - @_createDirectory basePath, (error) => - return callback(error) if error? - jobs = for resource in resources - do (resource) => - (callback) => @_writeResourceToDisk(project_id, resource, basePath, callback) - async.parallelLimit jobs, parallelFileDownloads, callback + saveIncrementalResourcesToDisk(project_id, resources, basePath, callback) { + if (callback == null) { callback = function(error) {}; } + return this._createDirectory(basePath, error => { + if (error != null) { return callback(error); } + const jobs = Array.from(resources).map((resource) => + (resource => { + return callback => this._writeResourceToDisk(project_id, resource, basePath, callback); + })(resource)); + return async.parallelLimit(jobs, parallelFileDownloads, callback); + }); + }, - saveAllResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> - @_createDirectory basePath, (error) => - return callback(error) if error? - @_removeExtraneousFiles resources, basePath, (error) => - return callback(error) if error? - jobs = for resource in resources - do (resource) => - (callback) => @_writeResourceToDisk(project_id, resource, basePath, callback) - async.parallelLimit jobs, parallelFileDownloads, callback + saveAllResourcesToDisk(project_id, resources, basePath, callback) { + if (callback == null) { callback = function(error) {}; } + return this._createDirectory(basePath, error => { + if (error != null) { return callback(error); } + return this._removeExtraneousFiles(resources, basePath, error => { + if (error != null) { return callback(error); } + const jobs = Array.from(resources).map((resource) => + (resource => { + return callback => this._writeResourceToDisk(project_id, resource, basePath, callback); + })(resource)); + return async.parallelLimit(jobs, parallelFileDownloads, callback); + }); + }); + }, - _createDirectory: (basePath, callback = (error) ->) -> - fs.mkdir basePath, (err) -> - if err? - if err.code is 'EEXIST' - return callback() - else - logger.log {err: err, dir:basePath}, "error creating directory" - return callback(err) - else - return callback() + _createDirectory(basePath, callback) { + if (callback == null) { callback = function(error) {}; } + return fs.mkdir(basePath, function(err) { + if (err != null) { + if (err.code === 'EEXIST') { + return callback(); + } else { + logger.log({err, dir:basePath}, "error creating directory"); + return callback(err); + } + } else { + return callback(); + } + }); + }, - _removeExtraneousFiles: (resources, basePath, _callback = (error, outputFiles, allFiles) ->) -> - timer = new Metrics.Timer("unlink-output-files") - callback = (error, result...) -> - timer.done() - _callback(error, result...) + _removeExtraneousFiles(resources, basePath, _callback) { + if (_callback == null) { _callback = function(error, outputFiles, allFiles) {}; } + const timer = new Metrics.Timer("unlink-output-files"); + const callback = function(error, ...result) { + timer.done(); + return _callback(error, ...Array.from(result)); + }; - OutputFileFinder.findOutputFiles resources, basePath, (error, outputFiles, allFiles) -> - return callback(error) if error? + return OutputFileFinder.findOutputFiles(resources, basePath, function(error, outputFiles, allFiles) { + if (error != null) { return callback(error); } - jobs = [] - for file in outputFiles or [] - do (file) -> - path = file.path - should_delete = true - if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache - should_delete = false - if path.match(/^output-.*/) # Tikz cached figures (default case) - should_delete = false - if path.match(/\.(pdf|dpth|md5)$/) # Tikz cached figures (by extension) - should_delete = false - if path.match(/\.(pygtex|pygstyle)$/) or path.match(/(^|\/)_minted-[^\/]+\//) # minted files/directory - should_delete = false - if path.match(/\.md\.tex$/) or path.match(/(^|\/)_markdown_[^\/]+\//) # markdown files/directory - should_delete = false - if path.match(/-eps-converted-to\.pdf$/) # Epstopdf generated files - should_delete = false - if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv" - should_delete = true - if path == "output.tex" # created by TikzManager if present in output files - should_delete = true - if should_delete - jobs.push (callback) -> ResourceWriter._deleteFileIfNotDirectory Path.join(basePath, path), callback + const jobs = []; + for (let file of Array.from(outputFiles || [])) { + (function(file) { + const { path } = file; + let should_delete = true; + if (path.match(/^output\./) || path.match(/\.aux$/) || path.match(/^cache\//)) { // knitr cache + should_delete = false; + } + if (path.match(/^output-.*/)) { // Tikz cached figures (default case) + should_delete = false; + } + if (path.match(/\.(pdf|dpth|md5)$/)) { // Tikz cached figures (by extension) + should_delete = false; + } + if (path.match(/\.(pygtex|pygstyle)$/) || path.match(/(^|\/)_minted-[^\/]+\//)) { // minted files/directory + should_delete = false; + } + if (path.match(/\.md\.tex$/) || path.match(/(^|\/)_markdown_[^\/]+\//)) { // markdown files/directory + should_delete = false; + } + if (path.match(/-eps-converted-to\.pdf$/)) { // Epstopdf generated files + should_delete = false; + } + if ((path === "output.pdf") || (path === "output.dvi") || (path === "output.log") || (path === "output.xdv")) { + should_delete = true; + } + if (path === "output.tex") { // created by TikzManager if present in output files + should_delete = true; + } + if (should_delete) { + return jobs.push(callback => ResourceWriter._deleteFileIfNotDirectory(Path.join(basePath, path), callback)); + } + })(file); + } - async.series jobs, (error) -> - return callback(error) if error? - callback(null, outputFiles, allFiles) + return async.series(jobs, function(error) { + if (error != null) { return callback(error); } + return callback(null, outputFiles, allFiles); + }); + }); + }, - _deleteFileIfNotDirectory: (path, callback = (error) ->) -> - fs.stat path, (error, stat) -> - if error? and error.code is 'ENOENT' - return callback() - else if error? - logger.err {err: error, path: path}, "error stating file in deleteFileIfNotDirectory" - return callback(error) - else if stat.isFile() - fs.unlink path, (error) -> - if error? - logger.err {err: error, path: path}, "error removing file in deleteFileIfNotDirectory" - callback(error) - else - callback() - else - callback() + _deleteFileIfNotDirectory(path, callback) { + if (callback == null) { callback = function(error) {}; } + return fs.stat(path, function(error, stat) { + if ((error != null) && (error.code === 'ENOENT')) { + return callback(); + } else if (error != null) { + logger.err({err: error, path}, "error stating file in deleteFileIfNotDirectory"); + return callback(error); + } else if (stat.isFile()) { + return fs.unlink(path, function(error) { + if (error != null) { + logger.err({err: error, path}, "error removing file in deleteFileIfNotDirectory"); + return callback(error); + } else { + return callback(); + } + }); + } else { + return callback(); + } + }); + }, - _writeResourceToDisk: (project_id, resource, basePath, callback = (error) ->) -> - ResourceWriter.checkPath basePath, resource.path, (error, path) -> - return callback(error) if error? - mkdirp Path.dirname(path), (error) -> - return callback(error) if error? - # TODO: Don't overwrite file if it hasn't been modified - if resource.url? - UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)-> - if err? - logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources" - callback() #try and continue compiling even if http resource can not be downloaded at this time - else - process = require("process") - fs.writeFile path, resource.content, callback - try - result = fs.lstatSync(path) - catch e + _writeResourceToDisk(project_id, resource, basePath, callback) { + if (callback == null) { callback = function(error) {}; } + return ResourceWriter.checkPath(basePath, resource.path, function(error, path) { + if (error != null) { return callback(error); } + return mkdirp(Path.dirname(path), function(error) { + if (error != null) { return callback(error); } + // TODO: Don't overwrite file if it hasn't been modified + if (resource.url != null) { + return UrlCache.downloadUrlToFile(project_id, resource.url, path, resource.modified, function(err){ + if (err != null) { + logger.err({err, project_id, path, resource_url:resource.url, modified:resource.modified}, "error downloading file for resources"); + } + return callback(); + }); //try and continue compiling even if http resource can not be downloaded at this time + } else { + const process = require("process"); + fs.writeFile(path, resource.content, callback); + try { + let result; + return result = fs.lstatSync(path); + } catch (e) {} + } + }); + }); + }, - checkPath: (basePath, resourcePath, callback) -> - path = Path.normalize(Path.join(basePath, resourcePath)) - if (path.slice(0, basePath.length + 1) != basePath + "/") - return callback new Error("resource path is outside root directory") - else - return callback(null, path) + checkPath(basePath, resourcePath, callback) { + const path = Path.normalize(Path.join(basePath, resourcePath)); + if (path.slice(0, basePath.length + 1) !== (basePath + "/")) { + return callback(new Error("resource path is outside root directory")); + } else { + return callback(null, path); + } + } +}); diff --git a/app/coffee/SafeReader.js b/app/coffee/SafeReader.js index adb96b16..f1a63494 100644 --- a/app/coffee/SafeReader.js +++ b/app/coffee/SafeReader.js @@ -1,25 +1,40 @@ -fs = require "fs" -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SafeReader; +const fs = require("fs"); +const logger = require("logger-sharelatex"); -module.exports = SafeReader = +module.exports = (SafeReader = { - # safely read up to size bytes from a file and return result as a - # string + // safely read up to size bytes from a file and return result as a + // string - readFile: (file, size, encoding, callback = (error, result) ->) -> - fs.open file, 'r', (err, fd) -> - return callback() if err? and err.code is 'ENOENT' - return callback(err) if err? + readFile(file, size, encoding, callback) { + if (callback == null) { callback = function(error, result) {}; } + return fs.open(file, 'r', function(err, fd) { + if ((err != null) && (err.code === 'ENOENT')) { return callback(); } + if (err != null) { return callback(err); } - # safely return always closing the file - callbackWithClose = (err, result...) -> - fs.close fd, (err1) -> - return callback(err) if err? - return callback(err1) if err1? - callback(null, result...) + // safely return always closing the file + const callbackWithClose = (err, ...result) => + fs.close(fd, function(err1) { + if (err != null) { return callback(err); } + if (err1 != null) { return callback(err1); } + return callback(null, ...Array.from(result)); + }) + ; - buff = new Buffer(size, 0) # fill with zeros - fs.read fd, buff, 0, buff.length, 0, (err, bytesRead, buffer) -> - return callbackWithClose(err) if err? - result = buffer.toString(encoding, 0, bytesRead) - callbackWithClose(null, result, bytesRead) + const buff = new Buffer(size, 0); // fill with zeros + return fs.read(fd, buff, 0, buff.length, 0, function(err, bytesRead, buffer) { + if (err != null) { return callbackWithClose(err); } + const result = buffer.toString(encoding, 0, bytesRead); + return callbackWithClose(null, result, bytesRead); + }); + }); + } +}); diff --git a/app/coffee/StaticServerForbidSymlinks.js b/app/coffee/StaticServerForbidSymlinks.js index 1b3cd458..90a8879d 100644 --- a/app/coffee/StaticServerForbidSymlinks.js +++ b/app/coffee/StaticServerForbidSymlinks.js @@ -1,41 +1,64 @@ -Path = require("path") -fs = require("fs") -Settings = require("settings-sharelatex") -logger = require("logger-sharelatex") -url = require "url" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ForbidSymlinks; +const Path = require("path"); +const fs = require("fs"); +const Settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); +const url = require("url"); -module.exports = ForbidSymlinks = (staticFn, root, options) -> - expressStatic = staticFn root, options - basePath = Path.resolve(root) - return (req, res, next) -> - path = url.parse(req.url)?.pathname - # check that the path is of the form /project_id_or_name/path/to/file.log - if result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/) - project_id = result[1] - file = result[2] - else - logger.warn path: path, "unrecognized file request" - return res.sendStatus(404) - # check that the file does not use a relative path - for dir in file.split('/') - if dir == '..' - logger.warn path: path, "attempt to use a relative path" - return res.sendStatus(404) - # check that the requested path is normalized - requestedFsPath = "#{basePath}/#{project_id}/#{file}" - if requestedFsPath != Path.normalize(requestedFsPath) - logger.error path: requestedFsPath, "requestedFsPath is not normalized" - return res.sendStatus(404) - # check that the requested path is not a symlink - fs.realpath requestedFsPath, (err, realFsPath)-> - if err? - if err.code == 'ENOENT' - return res.sendStatus(404) - else - logger.error err:err, requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "error checking file access" - return res.sendStatus(500) - else if requestedFsPath != realFsPath - logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting" - return res.sendStatus(404) - else - expressStatic(req, res, next) +module.exports = (ForbidSymlinks = function(staticFn, root, options) { + const expressStatic = staticFn(root, options); + const basePath = Path.resolve(root); + return function(req, res, next) { + let file, project_id, result; + const path = __guard__(url.parse(req.url), x => x.pathname); + // check that the path is of the form /project_id_or_name/path/to/file.log + if (result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/)) { + project_id = result[1]; + file = result[2]; + } else { + logger.warn({path}, "unrecognized file request"); + return res.sendStatus(404); + } + // check that the file does not use a relative path + for (let dir of Array.from(file.split('/'))) { + if (dir === '..') { + logger.warn({path}, "attempt to use a relative path"); + return res.sendStatus(404); + } + } + // check that the requested path is normalized + const requestedFsPath = `${basePath}/${project_id}/${file}`; + if (requestedFsPath !== Path.normalize(requestedFsPath)) { + logger.error({path: requestedFsPath}, "requestedFsPath is not normalized"); + return res.sendStatus(404); + } + // check that the requested path is not a symlink + return fs.realpath(requestedFsPath, function(err, realFsPath){ + if (err != null) { + if (err.code === 'ENOENT') { + return res.sendStatus(404); + } else { + logger.error({err, requestedFsPath, realFsPath, path: req.params[0], project_id: req.params.project_id}, "error checking file access"); + return res.sendStatus(500); + } + } else if (requestedFsPath !== realFsPath) { + logger.warn({requestedFsPath, realFsPath, path: req.params[0], project_id: req.params.project_id}, "trying to access a different file (symlink), aborting"); + return res.sendStatus(404); + } else { + return expressStatic(req, res, next); + } + }); + }; +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/app/coffee/TikzManager.js b/app/coffee/TikzManager.js index 22def278..fb526447 100644 --- a/app/coffee/TikzManager.js +++ b/app/coffee/TikzManager.js @@ -1,37 +1,56 @@ -fs = require "fs" -Path = require "path" -ResourceWriter = require "./ResourceWriter" -SafeReader = require "./SafeReader" -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TikzManager; +const fs = require("fs"); +const Path = require("path"); +const ResourceWriter = require("./ResourceWriter"); +const SafeReader = require("./SafeReader"); +const logger = require("logger-sharelatex"); -# for \tikzexternalize or pstool to work the main file needs to match the -# jobname. Since we set the -jobname to output, we have to create a -# copy of the main file as 'output.tex'. +// for \tikzexternalize or pstool to work the main file needs to match the +// jobname. Since we set the -jobname to output, we have to create a +// copy of the main file as 'output.tex'. -module.exports = TikzManager = +module.exports = (TikzManager = { - checkMainFile: (compileDir, mainFile, resources, callback = (error, needsMainFile) ->) -> - # if there's already an output.tex file, we don't want to touch it - for resource in resources - if resource.path is "output.tex" - logger.log compileDir: compileDir, mainFile: mainFile, "output.tex already in resources" - return callback(null, false) - # if there's no output.tex, see if we are using tikz/pgf or pstool in the main file - ResourceWriter.checkPath compileDir, mainFile, (error, path) -> - return callback(error) if error? - SafeReader.readFile path, 65536, "utf8", (error, content) -> - return callback(error) if error? - usesTikzExternalize = content?.indexOf("\\tikzexternalize") >= 0 - usesPsTool = content?.indexOf("{pstool}") >= 0 - logger.log compileDir: compileDir, mainFile: mainFile, usesTikzExternalize:usesTikzExternalize, usesPsTool: usesPsTool, "checked for packages needing main file as output.tex" - needsMainFile = (usesTikzExternalize || usesPsTool) - callback null, needsMainFile + checkMainFile(compileDir, mainFile, resources, callback) { + // if there's already an output.tex file, we don't want to touch it + if (callback == null) { callback = function(error, needsMainFile) {}; } + for (let resource of Array.from(resources)) { + if (resource.path === "output.tex") { + logger.log({compileDir, mainFile}, "output.tex already in resources"); + return callback(null, false); + } + } + // if there's no output.tex, see if we are using tikz/pgf or pstool in the main file + return ResourceWriter.checkPath(compileDir, mainFile, function(error, path) { + if (error != null) { return callback(error); } + return SafeReader.readFile(path, 65536, "utf8", function(error, content) { + if (error != null) { return callback(error); } + const usesTikzExternalize = (content != null ? content.indexOf("\\tikzexternalize") : undefined) >= 0; + const usesPsTool = (content != null ? content.indexOf("{pstool}") : undefined) >= 0; + logger.log({compileDir, mainFile, usesTikzExternalize, usesPsTool}, "checked for packages needing main file as output.tex"); + const needsMainFile = (usesTikzExternalize || usesPsTool); + return callback(null, needsMainFile); + }); + }); + }, - injectOutputFile: (compileDir, mainFile, callback = (error) ->) -> - ResourceWriter.checkPath compileDir, mainFile, (error, path) -> - return callback(error) if error? - fs.readFile path, "utf8", (error, content) -> - return callback(error) if error? - logger.log compileDir: compileDir, mainFile: mainFile, "copied file to output.tex as project uses packages which require it" - # use wx flag to ensure that output file does not already exist - fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback + injectOutputFile(compileDir, mainFile, callback) { + if (callback == null) { callback = function(error) {}; } + return ResourceWriter.checkPath(compileDir, mainFile, function(error, path) { + if (error != null) { return callback(error); } + return fs.readFile(path, "utf8", function(error, content) { + if (error != null) { return callback(error); } + logger.log({compileDir, mainFile}, "copied file to output.tex as project uses packages which require it"); + // use wx flag to ensure that output file does not already exist + return fs.writeFile(Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback); + }); + }); + } +}); diff --git a/app/coffee/UrlCache.js b/app/coffee/UrlCache.js index d44479a1..9a199688 100644 --- a/app/coffee/UrlCache.js +++ b/app/coffee/UrlCache.js @@ -1,134 +1,189 @@ -db = require("./db") -dbQueue = require "./DbQueue" -UrlFetcher = require("./UrlFetcher") -Settings = require("settings-sharelatex") -crypto = require("crypto") -fs = require("fs") -logger = require "logger-sharelatex" -async = require "async" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UrlCache; +const db = require("./db"); +const dbQueue = require("./DbQueue"); +const UrlFetcher = require("./UrlFetcher"); +const Settings = require("settings-sharelatex"); +const crypto = require("crypto"); +const fs = require("fs"); +const logger = require("logger-sharelatex"); +const async = require("async"); -module.exports = UrlCache = - downloadUrlToFile: (project_id, url, destPath, lastModified, callback = (error) ->) -> - UrlCache._ensureUrlIsInCache project_id, url, lastModified, (error, pathToCachedUrl) => - return callback(error) if error? - UrlCache._copyFile pathToCachedUrl, destPath, (error) -> - if error? - UrlCache._clearUrlDetails project_id, url, () -> - callback(error) - else - callback(error) +module.exports = (UrlCache = { + downloadUrlToFile(project_id, url, destPath, lastModified, callback) { + if (callback == null) { callback = function(error) {}; } + return UrlCache._ensureUrlIsInCache(project_id, url, lastModified, (error, pathToCachedUrl) => { + if (error != null) { return callback(error); } + return UrlCache._copyFile(pathToCachedUrl, destPath, function(error) { + if (error != null) { + return UrlCache._clearUrlDetails(project_id, url, () => callback(error)); + } else { + return callback(error); + } + }); + }); + }, - clearProject: (project_id, callback = (error) ->) -> - UrlCache._findAllUrlsInProject project_id, (error, urls) -> - logger.log project_id: project_id, url_count: urls.length, "clearing project URLs" - return callback(error) if error? - jobs = for url in (urls or []) - do (url) -> - (callback) -> - UrlCache._clearUrlFromCache project_id, url, (error) -> - if error? - logger.error err: error, project_id: project_id, url: url, "error clearing project URL" - callback() - async.series jobs, callback + clearProject(project_id, callback) { + if (callback == null) { callback = function(error) {}; } + return UrlCache._findAllUrlsInProject(project_id, function(error, urls) { + logger.log({project_id, url_count: urls.length}, "clearing project URLs"); + if (error != null) { return callback(error); } + const jobs = (Array.from(urls || [])).map((url) => + (url => + callback => + UrlCache._clearUrlFromCache(project_id, url, function(error) { + if (error != null) { + logger.error({err: error, project_id, url}, "error clearing project URL"); + } + return callback(); + }) + + )(url)); + return async.series(jobs, callback); + }); + }, - _ensureUrlIsInCache: (project_id, url, lastModified, callback = (error, pathOnDisk) ->) -> - if lastModified? - # MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds. - # So round down to seconds - lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000) - UrlCache._doesUrlNeedDownloading project_id, url, lastModified, (error, needsDownloading) => - return callback(error) if error? - if needsDownloading - logger.log url: url, lastModified: lastModified, "downloading URL" - UrlFetcher.pipeUrlToFile url, UrlCache._cacheFilePathForUrl(project_id, url), (error) => - return callback(error) if error? - UrlCache._updateOrCreateUrlDetails project_id, url, lastModified, (error) => - return callback(error) if error? - callback null, UrlCache._cacheFilePathForUrl(project_id, url) - else - logger.log url: url, lastModified: lastModified, "URL is up to date in cache" - callback null, UrlCache._cacheFilePathForUrl(project_id, url) + _ensureUrlIsInCache(project_id, url, lastModified, callback) { + if (callback == null) { callback = function(error, pathOnDisk) {}; } + if (lastModified != null) { + // MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds. + // So round down to seconds + lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000); + } + return UrlCache._doesUrlNeedDownloading(project_id, url, lastModified, (error, needsDownloading) => { + if (error != null) { return callback(error); } + if (needsDownloading) { + logger.log({url, lastModified}, "downloading URL"); + return UrlFetcher.pipeUrlToFile(url, UrlCache._cacheFilePathForUrl(project_id, url), error => { + if (error != null) { return callback(error); } + return UrlCache._updateOrCreateUrlDetails(project_id, url, lastModified, error => { + if (error != null) { return callback(error); } + return callback(null, UrlCache._cacheFilePathForUrl(project_id, url)); + }); + }); + } else { + logger.log({url, lastModified}, "URL is up to date in cache"); + return callback(null, UrlCache._cacheFilePathForUrl(project_id, url)); + } + }); + }, - _doesUrlNeedDownloading: (project_id, url, lastModified, callback = (error, needsDownloading) ->) -> - if !lastModified? - return callback null, true - UrlCache._findUrlDetails project_id, url, (error, urlDetails) -> - return callback(error) if error? - if !urlDetails? or !urlDetails.lastModified? or urlDetails.lastModified.getTime() < lastModified.getTime() - return callback null, true - else - return callback null, false + _doesUrlNeedDownloading(project_id, url, lastModified, callback) { + if (callback == null) { callback = function(error, needsDownloading) {}; } + if ((lastModified == null)) { + return callback(null, true); + } + return UrlCache._findUrlDetails(project_id, url, function(error, urlDetails) { + if (error != null) { return callback(error); } + if ((urlDetails == null) || (urlDetails.lastModified == null) || (urlDetails.lastModified.getTime() < lastModified.getTime())) { + return callback(null, true); + } else { + return callback(null, false); + } + }); + }, - _cacheFileNameForUrl: (project_id, url) -> - project_id + ":" + crypto.createHash("md5").update(url).digest("hex") + _cacheFileNameForUrl(project_id, url) { + return project_id + ":" + crypto.createHash("md5").update(url).digest("hex"); + }, - _cacheFilePathForUrl: (project_id, url) -> - "#{Settings.path.clsiCacheDir}/#{UrlCache._cacheFileNameForUrl(project_id, url)}" + _cacheFilePathForUrl(project_id, url) { + return `${Settings.path.clsiCacheDir}/${UrlCache._cacheFileNameForUrl(project_id, url)}`; + }, - _copyFile: (from, to, _callback = (error) ->) -> - callbackOnce = (error) -> - if error? - logger.error err: error, from:from, to:to, "error copying file from cache" - _callback(error) - _callback = () -> - writeStream = fs.createWriteStream(to) - readStream = fs.createReadStream(from) - writeStream.on "error", callbackOnce - readStream.on "error", callbackOnce - writeStream.on "close", callbackOnce - writeStream.on "open", () -> - readStream.pipe(writeStream) + _copyFile(from, to, _callback) { + if (_callback == null) { _callback = function(error) {}; } + const callbackOnce = function(error) { + if (error != null) { + logger.error({err: error, from, to}, "error copying file from cache"); + } + _callback(error); + return _callback = function() {}; + }; + const writeStream = fs.createWriteStream(to); + const readStream = fs.createReadStream(from); + writeStream.on("error", callbackOnce); + readStream.on("error", callbackOnce); + writeStream.on("close", callbackOnce); + return writeStream.on("open", () => readStream.pipe(writeStream)); + }, - _clearUrlFromCache: (project_id, url, callback = (error) ->) -> - UrlCache._clearUrlDetails project_id, url, (error) -> - return callback(error) if error? - UrlCache._deleteUrlCacheFromDisk project_id, url, (error) -> - return callback(error) if error? - callback null + _clearUrlFromCache(project_id, url, callback) { + if (callback == null) { callback = function(error) {}; } + return UrlCache._clearUrlDetails(project_id, url, function(error) { + if (error != null) { return callback(error); } + return UrlCache._deleteUrlCacheFromDisk(project_id, url, function(error) { + if (error != null) { return callback(error); } + return callback(null); + }); + }); + }, - _deleteUrlCacheFromDisk: (project_id, url, callback = (error) ->) -> - fs.unlink UrlCache._cacheFilePathForUrl(project_id, url), (error) -> - if error? and error.code != 'ENOENT' # no error if the file isn't present - return callback(error) - else - return callback() + _deleteUrlCacheFromDisk(project_id, url, callback) { + if (callback == null) { callback = function(error) {}; } + return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function(error) { + if ((error != null) && (error.code !== 'ENOENT')) { // no error if the file isn't present + return callback(error); + } else { + return callback(); + } + }); + }, - _findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) -> - job = (cb)-> - db.UrlCache.find(where: { url: url, project_id: project_id }) - .then((urlDetails) -> cb null, urlDetails) - .error cb - dbQueue.queue.push job, callback + _findUrlDetails(project_id, url, callback) { + if (callback == null) { callback = function(error, urlDetails) {}; } + const job = cb=> + db.UrlCache.find({where: { url, project_id }}) + .then(urlDetails => cb(null, urlDetails)) + .error(cb) + ; + return dbQueue.queue.push(job, callback); + }, - _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) -> - job = (cb)-> - db.UrlCache.findOrCreate(where: {url: url, project_id: project_id}) + _updateOrCreateUrlDetails(project_id, url, lastModified, callback) { + if (callback == null) { callback = function(error) {}; } + const job = cb=> + db.UrlCache.findOrCreate({where: {url, project_id}}) .spread( - (urlDetails, created) -> - urlDetails.updateAttributes(lastModified: lastModified) - .then(() -> cb()) + (urlDetails, created) => + urlDetails.updateAttributes({lastModified}) + .then(() => cb()) .error(cb) ) - .error cb - dbQueue.queue.push(job, callback) + .error(cb) + ; + return dbQueue.queue.push(job, callback); + }, - _clearUrlDetails: (project_id, url, callback = (error) ->) -> - job = (cb)-> - db.UrlCache.destroy(where: {url: url, project_id: project_id}) - .then(() -> cb null) - .error cb - dbQueue.queue.push(job, callback) + _clearUrlDetails(project_id, url, callback) { + if (callback == null) { callback = function(error) {}; } + const job = cb=> + db.UrlCache.destroy({where: {url, project_id}}) + .then(() => cb(null)) + .error(cb) + ; + return dbQueue.queue.push(job, callback); + }, - _findAllUrlsInProject: (project_id, callback = (error, urls) ->) -> - job = (cb)-> - db.UrlCache.findAll(where: { project_id: project_id }) + _findAllUrlsInProject(project_id, callback) { + if (callback == null) { callback = function(error, urls) {}; } + const job = cb=> + db.UrlCache.findAll({where: { project_id }}) .then( - (urlEntries) -> - cb null, urlEntries.map((entry) -> entry.url) - ) - .error cb - dbQueue.queue.push(job, callback) + urlEntries => cb(null, urlEntries.map(entry => entry.url))) + .error(cb) + ; + return dbQueue.queue.push(job, callback); + } +}); diff --git a/app/coffee/UrlFetcher.js b/app/coffee/UrlFetcher.js index da10859f..ea209560 100644 --- a/app/coffee/UrlFetcher.js +++ b/app/coffee/UrlFetcher.js @@ -1,70 +1,88 @@ -request = require("request").defaults(jar: false) -fs = require("fs") -logger = require "logger-sharelatex" -settings = require("settings-sharelatex") -URL = require('url'); +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UrlFetcher; +const request = require("request").defaults({jar: false}); +const fs = require("fs"); +const logger = require("logger-sharelatex"); +const settings = require("settings-sharelatex"); +const URL = require('url'); -oneMinute = 60 * 1000 +const oneMinute = 60 * 1000; -module.exports = UrlFetcher = - pipeUrlToFile: (url, filePath, _callback = (error) ->) -> - callbackOnce = (error) -> - clearTimeout timeoutHandler if timeoutHandler? - _callback(error) - _callback = () -> +module.exports = (UrlFetcher = { + pipeUrlToFile(url, filePath, _callback) { + if (_callback == null) { _callback = function(error) {}; } + const callbackOnce = function(error) { + if (timeoutHandler != null) { clearTimeout(timeoutHandler); } + _callback(error); + return _callback = function() {}; + }; - if settings.filestoreDomainOveride? - p = URL.parse(url).path - url = "#{settings.filestoreDomainOveride}#{p}" - timeoutHandler = setTimeout () -> - timeoutHandler = null - logger.error url:url, filePath: filePath, "Timed out downloading file to cache" - callbackOnce(new Error("Timed out downloading file to cache #{url}")) - # FIXME: maybe need to close fileStream here - , 3 * oneMinute + if (settings.filestoreDomainOveride != null) { + const p = URL.parse(url).path; + url = `${settings.filestoreDomainOveride}${p}`; + } + var timeoutHandler = setTimeout(function() { + timeoutHandler = null; + logger.error({url, filePath}, "Timed out downloading file to cache"); + return callbackOnce(new Error(`Timed out downloading file to cache ${url}`)); + } + // FIXME: maybe need to close fileStream here + , 3 * oneMinute); - logger.log url:url, filePath: filePath, "started downloading url to cache" - urlStream = request.get({url: url, timeout: oneMinute}) - urlStream.pause() # stop data flowing until we are ready + logger.log({url, filePath}, "started downloading url to cache"); + const urlStream = request.get({url, timeout: oneMinute}); + urlStream.pause(); // stop data flowing until we are ready - # attach handlers before setting up pipes - urlStream.on "error", (error) -> - logger.error err: error, url:url, filePath: filePath, "error downloading url" - callbackOnce(error or new Error("Something went wrong downloading the URL #{url}")) + // attach handlers before setting up pipes + urlStream.on("error", function(error) { + logger.error({err: error, url, filePath}, "error downloading url"); + return callbackOnce(error || new Error(`Something went wrong downloading the URL ${url}`)); + }); - urlStream.on "end", () -> - logger.log url:url, filePath: filePath, "finished downloading file into cache" + urlStream.on("end", () => logger.log({url, filePath}, "finished downloading file into cache")); - urlStream.on "response", (res) -> - if res.statusCode >= 200 and res.statusCode < 300 - fileStream = fs.createWriteStream(filePath) + return urlStream.on("response", function(res) { + if ((res.statusCode >= 200) && (res.statusCode < 300)) { + const fileStream = fs.createWriteStream(filePath); - # attach handlers before setting up pipes - fileStream.on 'error', (error) -> - logger.error err: error, url:url, filePath: filePath, "error writing file into cache" - fs.unlink filePath, (err) -> - if err? - logger.err err: err, filePath: filePath, "error deleting file from cache" - callbackOnce(error) + // attach handlers before setting up pipes + fileStream.on('error', function(error) { + logger.error({err: error, url, filePath}, "error writing file into cache"); + return fs.unlink(filePath, function(err) { + if (err != null) { + logger.err({err, filePath}, "error deleting file from cache"); + } + return callbackOnce(error); + }); + }); - fileStream.on 'finish', () -> - logger.log url:url, filePath: filePath, "finished writing file into cache" - callbackOnce() + fileStream.on('finish', function() { + logger.log({url, filePath}, "finished writing file into cache"); + return callbackOnce(); + }); - fileStream.on 'pipe', () -> - logger.log url:url, filePath: filePath, "piping into filestream" + fileStream.on('pipe', () => logger.log({url, filePath}, "piping into filestream")); - urlStream.pipe(fileStream) - urlStream.resume() # now we are ready to handle the data - else - logger.error statusCode: res.statusCode, url:url, filePath: filePath, "unexpected status code downloading url to cache" - # https://nodejs.org/api/http.html#http_class_http_clientrequest - # If you add a 'response' event handler, then you must consume - # the data from the response object, either by calling - # response.read() whenever there is a 'readable' event, or by - # adding a 'data' handler, or by calling the .resume() - # method. Until the data is consumed, the 'end' event will not - # fire. Also, until the data is read it will consume memory - # that can eventually lead to a 'process out of memory' error. - urlStream.resume() # discard the data - callbackOnce(new Error("URL returned non-success status code: #{res.statusCode} #{url}")) + urlStream.pipe(fileStream); + return urlStream.resume(); // now we are ready to handle the data + } else { + logger.error({statusCode: res.statusCode, url, filePath}, "unexpected status code downloading url to cache"); + // https://nodejs.org/api/http.html#http_class_http_clientrequest + // If you add a 'response' event handler, then you must consume + // the data from the response object, either by calling + // response.read() whenever there is a 'readable' event, or by + // adding a 'data' handler, or by calling the .resume() + // method. Until the data is consumed, the 'end' event will not + // fire. Also, until the data is read it will consume memory + // that can eventually lead to a 'process out of memory' error. + urlStream.resume(); // discard the data + return callbackOnce(new Error(`URL returned non-success status code: ${res.statusCode} ${url}`)); + } + }); + } +}); diff --git a/app/coffee/db.js b/app/coffee/db.js index de48dfdf..385ad8d3 100644 --- a/app/coffee/db.js +++ b/app/coffee/db.js @@ -1,55 +1,59 @@ -Sequelize = require("sequelize") -Settings = require("settings-sharelatex") -_ = require("underscore") -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Sequelize = require("sequelize"); +const Settings = require("settings-sharelatex"); +const _ = require("underscore"); +const logger = require("logger-sharelatex"); -options = _.extend {logging:false}, Settings.mysql.clsi +const options = _.extend({logging:false}, Settings.mysql.clsi); -logger.log dbPath:Settings.mysql.clsi.storage, "connecting to db" +logger.log({dbPath:Settings.mysql.clsi.storage}, "connecting to db"); -sequelize = new Sequelize( +const sequelize = new Sequelize( Settings.mysql.clsi.database, Settings.mysql.clsi.username, Settings.mysql.clsi.password, options -) +); -if Settings.mysql.clsi.dialect == "sqlite" - logger.log "running PRAGMA journal_mode=WAL;" - sequelize.query("PRAGMA journal_mode=WAL;") - sequelize.query("PRAGMA synchronous=OFF;") - sequelize.query("PRAGMA read_uncommitted = true;") +if (Settings.mysql.clsi.dialect === "sqlite") { + logger.log("running PRAGMA journal_mode=WAL;"); + sequelize.query("PRAGMA journal_mode=WAL;"); + sequelize.query("PRAGMA synchronous=OFF;"); + sequelize.query("PRAGMA read_uncommitted = true;"); +} -module.exports = +module.exports = { UrlCache: sequelize.define("UrlCache", { - url: Sequelize.STRING - project_id: Sequelize.STRING + url: Sequelize.STRING, + project_id: Sequelize.STRING, lastModified: Sequelize.DATE }, { indexes: [ {fields: ['url', 'project_id']}, {fields: ['project_id']} ] - }) + }), Project: sequelize.define("Project", { - project_id: {type: Sequelize.STRING, primaryKey: true} + project_id: {type: Sequelize.STRING, primaryKey: true}, lastAccessed: Sequelize.DATE }, { indexes: [ {fields: ['lastAccessed']} ] - }) + }), - op: Sequelize.Op + op: Sequelize.Op, - sync: () -> - logger.log dbPath:Settings.mysql.clsi.storage, "syncing db schema" - sequelize.sync() - .then(-> - logger.log "db sync complete" - ).catch((err)-> - console.log err, "error syncing" - ) + sync() { + logger.log({dbPath:Settings.mysql.clsi.storage}, "syncing db schema"); + return sequelize.sync() + .then(() => logger.log("db sync complete")).catch(err=> console.log(err, "error syncing")); + } +}; From c056ca6968f0cafeb4e40893f692b3e42178f52f Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:14:28 +0100 Subject: [PATCH 472/709] decaffeinate: Run post-processing cleanups on CommandRunner.coffee and 25 other files --- app/coffee/CommandRunner.js | 2 ++ app/coffee/CompileController.js | 7 ++++ app/coffee/CompileManager.js | 43 ++++++++++++++---------- app/coffee/ContentTypeMapper.js | 5 +++ app/coffee/DbQueue.js | 2 ++ app/coffee/DockerLockManager.js | 5 +++ app/coffee/DockerRunner.js | 22 ++++++++---- app/coffee/DraftModeManager.js | 7 ++++ app/coffee/Errors.js | 6 ++++ app/coffee/LatexRunner.js | 8 +++++ app/coffee/LocalCommandRunner.js | 8 +++++ app/coffee/LockManager.js | 6 ++++ app/coffee/Metrics.js | 2 ++ app/coffee/OutputCacheManager.js | 5 +++ app/coffee/OutputFileFinder.js | 12 +++++-- app/coffee/OutputFileOptimiser.js | 9 +++++ app/coffee/ProjectPersistenceManager.js | 6 ++++ app/coffee/RequestParser.js | 13 ++++++- app/coffee/ResourceStateManager.js | 16 ++++++--- app/coffee/ResourceWriter.js | 13 +++++-- app/coffee/SafeReader.js | 7 ++++ app/coffee/StaticServerForbidSymlinks.js | 10 +++++- app/coffee/TikzManager.js | 8 ++++- app/coffee/UrlCache.js | 7 ++++ app/coffee/UrlFetcher.js | 8 +++++ app/coffee/db.js | 5 +++ 26 files changed, 206 insertions(+), 36 deletions(-) diff --git a/app/coffee/CommandRunner.js b/app/coffee/CommandRunner.js index dd7210a3..1c46ee95 100644 --- a/app/coffee/CommandRunner.js +++ b/app/coffee/CommandRunner.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS207: Consider shorter variations of null checks diff --git a/app/coffee/CompileController.js b/app/coffee/CompileController.js index cfdbcfe8..60925fc1 100644 --- a/app/coffee/CompileController.js +++ b/app/coffee/CompileController.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/CompileManager.js b/app/coffee/CompileManager.js index 82dafd12..76fb8b04 100644 --- a/app/coffee/CompileManager.js +++ b/app/coffee/CompileManager.js @@ -1,3 +1,12 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -88,13 +97,13 @@ module.exports = (CompileManager = { // only run chktex on LaTeX files (not knitr .Rtex files or any others) const isLaTeXFile = request.rootResourcePath != null ? request.rootResourcePath.match(/\.tex$/i) : undefined; if ((request.check != null) && isLaTeXFile) { - env['CHKTEX_OPTIONS'] = '-nall -e9 -e10 -w15 -w16'; - env['CHKTEX_ULIMIT_OPTIONS'] = '-t 5 -v 64000'; + env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16'; + env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000'; if (request.check === 'error') { - env['CHKTEX_EXIT_ON_ERROR'] = 1; + env.CHKTEX_EXIT_ON_ERROR = 1; } if (request.check === 'validate') { - env['CHKTEX_VALIDATE'] = 1; + env.CHKTEX_VALIDATE = 1; } } @@ -337,7 +346,7 @@ module.exports = (CompileManager = { _parseSynctexFromCodeOutput(output) { const results = []; - for (let line of Array.from(output.split("\n"))) { + for (const line of Array.from(output.split("\n"))) { const [node, page, h, v, width, height] = Array.from(line.split("\t")); if (node === "NODE") { results.push({ @@ -387,7 +396,7 @@ module.exports = (CompileManager = { if (error != null) { return callback(error); } return fs.readFile(compileDir + "/" + file_name + ".wc", "utf-8", function(err, stdout) { if (err != null) { - //call it node_err so sentry doesn't use random path error as unique id so it can't be ignored + // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored logger.err({node_err:err, command, compileDir, project_id, user_id}, "error reading word count output"); return callback(err); } @@ -412,37 +421,37 @@ module.exports = (CompileManager = { errors: 0, messages: "" }; - for (let line of Array.from(output.split("\n"))) { + for (const line of Array.from(output.split("\n"))) { const [data, info] = Array.from(line.split(":")); if (data.indexOf("Encoding") > -1) { - results['encode'] = info.trim(); + results.encode = info.trim(); } if (data.indexOf("in text") > -1) { - results['textWords'] = parseInt(info, 10); + results.textWords = parseInt(info, 10); } if (data.indexOf("in head") > -1) { - results['headWords'] = parseInt(info, 10); + results.headWords = parseInt(info, 10); } if (data.indexOf("outside") > -1) { - results['outside'] = parseInt(info, 10); + results.outside = parseInt(info, 10); } if (data.indexOf("of head") > -1) { - results['headers'] = parseInt(info, 10); + results.headers = parseInt(info, 10); } if (data.indexOf("Number of floats/tables/figures") > -1) { - results['elements'] = parseInt(info, 10); + results.elements = parseInt(info, 10); } if (data.indexOf("Number of math inlines") > -1) { - results['mathInline'] = parseInt(info, 10); + results.mathInline = parseInt(info, 10); } if (data.indexOf("Number of math displayed") > -1) { - results['mathDisplay'] = parseInt(info, 10); + results.mathDisplay = parseInt(info, 10); } if (data === "(errors") { // errors reported as (errors:123) - results['errors'] = parseInt(info, 10); + results.errors = parseInt(info, 10); } if (line.indexOf("!!! ") > -1) { // errors logged as !!! message !!! - results['messages'] += line + "\n"; + results.messages += line + "\n"; } } return results; diff --git a/app/coffee/ContentTypeMapper.js b/app/coffee/ContentTypeMapper.js index c57057f9..fdd66d38 100644 --- a/app/coffee/ContentTypeMapper.js +++ b/app/coffee/ContentTypeMapper.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. let ContentTypeMapper; const Path = require('path'); diff --git a/app/coffee/DbQueue.js b/app/coffee/DbQueue.js index 0f1f8cf1..89ff3234 100644 --- a/app/coffee/DbQueue.js +++ b/app/coffee/DbQueue.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/app/coffee/DockerLockManager.js b/app/coffee/DockerLockManager.js index 9c7deffe..274ff66c 100644 --- a/app/coffee/DockerLockManager.js +++ b/app/coffee/DockerLockManager.js @@ -1,3 +1,8 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/DockerRunner.js b/app/coffee/DockerRunner.js index ab78419e..dc04b5df 100644 --- a/app/coffee/DockerRunner.js +++ b/app/coffee/DockerRunner.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -140,7 +148,7 @@ module.exports = (DockerRunner = { return callback(err); } containerReturned = true; - __guard__(options != null ? options.HostConfig : undefined, x => x.SecurityOpt = null); //small log line + __guard__(options != null ? options.HostConfig : undefined, x => x.SecurityOpt = null); // small log line logger.log({err, exitCode, options}, "docker container has exited"); return callbackIfFinished(); }); @@ -164,7 +172,7 @@ module.exports = (DockerRunner = { // merge settings and environment parameter const env = {}; - for (let src of [Settings.clsi.docker.env, environment || {}]) { + for (const src of [Settings.clsi.docker.env, environment || {}]) { for (key in src) { value = src[key]; env[key] = value; } } // set the path based on the image year @@ -173,7 +181,7 @@ module.exports = (DockerRunner = { } else { year = "2014"; } - env['PATH'] = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/`; + env.PATH = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/`; const options = { "Cmd" : command, "Image" : image, @@ -208,7 +216,7 @@ module.exports = (DockerRunner = { if ((Settings.path != null ? Settings.path.synctexBinHostPath : undefined) != null) { - options["HostConfig"]["Binds"].push(`${Settings.path.synctexBinHostPath}:/opt/synctex:ro`); + options.HostConfig.Binds.push(`${Settings.path.synctexBinHostPath}:/opt/synctex:ro`); } if (Settings.clsi.docker.seccomp_profile != null) { @@ -254,7 +262,7 @@ module.exports = (DockerRunner = { }) ; const jobs = []; - for (let vol in volumes) { + for (const vol in volumes) { (vol => jobs.push(cb => checkVolume(vol, cb)))(vol); } return async.series(jobs, callback); @@ -279,7 +287,7 @@ module.exports = (DockerRunner = { DockerRunner.attachToContainer(options.name, attachStreamHandler, function(error){ if (error != null) { return callback(error); } return container.start(function(error) { - if ((error != null) && ((error != null ? error.statusCode : undefined) !== 304)) { //already running + if ((error != null) && ((error != null ? error.statusCode : undefined) !== 304)) { // already running return callback(error); } else { return callback(); @@ -434,7 +442,7 @@ module.exports = (DockerRunner = { return dockerode.listContainers({all: true}, function(error, containers) { if (error != null) { return callback(error); } const jobs = []; - for (let container of Array.from(containers || [])) { + for (const container of Array.from(containers || [])) { (container => DockerRunner.examineOldContainer(container, function(err, name, id, ttl) { if ((name.slice(0, 9) === '/project-') && (ttl <= 0)) { diff --git a/app/coffee/DraftModeManager.js b/app/coffee/DraftModeManager.js index 8ddbbd02..79f39ab2 100644 --- a/app/coffee/DraftModeManager.js +++ b/app/coffee/DraftModeManager.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/app/coffee/Errors.js b/app/coffee/Errors.js index 3a9ef220..e7ace2c2 100644 --- a/app/coffee/Errors.js +++ b/app/coffee/Errors.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-proto, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. let Errors; var NotFoundError = function(message) { const error = new Error(message); diff --git a/app/coffee/LatexRunner.js b/app/coffee/LatexRunner.js index 4c83e084..e569df8d 100644 --- a/app/coffee/LatexRunner.js +++ b/app/coffee/LatexRunner.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/app/coffee/LocalCommandRunner.js b/app/coffee/LocalCommandRunner.js index 405c51bd..24c0d8ea 100644 --- a/app/coffee/LocalCommandRunner.js +++ b/app/coffee/LocalCommandRunner.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/LockManager.js b/app/coffee/LockManager.js index 2405e8ac..8930fab6 100644 --- a/app/coffee/LockManager.js +++ b/app/coffee/LockManager.js @@ -1,3 +1,9 @@ +/* eslint-disable + handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/Metrics.js b/app/coffee/Metrics.js index 8148d664..94623da3 100644 --- a/app/coffee/Metrics.js +++ b/app/coffee/Metrics.js @@ -1,2 +1,4 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. module.exports = require("metrics-sharelatex"); diff --git a/app/coffee/OutputCacheManager.js b/app/coffee/OutputCacheManager.js index 6d03a106..b1bda0e8 100644 --- a/app/coffee/OutputCacheManager.js +++ b/app/coffee/OutputCacheManager.js @@ -1,3 +1,8 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/OutputFileFinder.js b/app/coffee/OutputFileFinder.js index f0f837c0..21a75874 100644 --- a/app/coffee/OutputFileFinder.js +++ b/app/coffee/OutputFileFinder.js @@ -1,3 +1,11 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -17,7 +25,7 @@ module.exports = (OutputFileFinder = { findOutputFiles(resources, directory, callback) { if (callback == null) { callback = function(error, outputFiles, allFiles) {}; } const incomingResources = {}; - for (let resource of Array.from(resources)) { + for (const resource of Array.from(resources)) { incomingResources[resource.path] = true; } @@ -28,7 +36,7 @@ module.exports = (OutputFileFinder = { return callback(error); } const outputFiles = []; - for (let file of Array.from(allFiles)) { + for (const file of Array.from(allFiles)) { if (!incomingResources[file]) { outputFiles.push({ path: file, diff --git a/app/coffee/OutputFileOptimiser.js b/app/coffee/OutputFileOptimiser.js index f8302aac..149d3846 100644 --- a/app/coffee/OutputFileOptimiser.js +++ b/app/coffee/OutputFileOptimiser.js @@ -1,3 +1,12 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, + no-undef, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/app/coffee/ProjectPersistenceManager.js b/app/coffee/ProjectPersistenceManager.js index 7b3d5ee2..856c1566 100644 --- a/app/coffee/ProjectPersistenceManager.js +++ b/app/coffee/ProjectPersistenceManager.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/RequestParser.js b/app/coffee/RequestParser.js index fdfb8bf4..6641086a 100644 --- a/app/coffee/RequestParser.js +++ b/app/coffee/RequestParser.js @@ -1,3 +1,14 @@ +/* eslint-disable + handle-callback-err, + no-control-regex, + no-throw-literal, + no-unused-vars, + no-useless-escape, + standard/no-callback-literal, + valid-typeof, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -172,7 +183,7 @@ module.exports = (RequestParser = { _checkPath(path) { // check that the request does not use a relative path - for (let dir of Array.from(path.split('/'))) { + for (const dir of Array.from(path.split('/'))) { if (dir === '..') { throw "relative path in root resource"; } diff --git a/app/coffee/ResourceStateManager.js b/app/coffee/ResourceStateManager.js index f430c8fd..45cfdc61 100644 --- a/app/coffee/ResourceStateManager.js +++ b/app/coffee/ResourceStateManager.js @@ -1,3 +1,9 @@ +/* eslint-disable + handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -62,10 +68,10 @@ module.exports = (ResourceStateManager = { if (bytesRead === size) { logger.error({file:stateFile, size, bytesRead}, "project state file truncated"); } - const array = __guard__(result != null ? result.toString() : undefined, x => x.split("\n")) || [], - adjustedLength = Math.max(array.length, 1), - resourceList = array.slice(0, adjustedLength - 1), - oldState = array[adjustedLength - 1]; + const array = __guard__(result != null ? result.toString() : undefined, x => x.split("\n")) || []; + const adjustedLength = Math.max(array.length, 1); + const resourceList = array.slice(0, adjustedLength - 1); + const oldState = array[adjustedLength - 1]; const newState = `stateHash:${state}`; logger.log({state, oldState, basePath, stateMatches: (newState === oldState)}, "checking sync state"); if (newState !== oldState) { @@ -82,7 +88,7 @@ module.exports = (ResourceStateManager = { let file; if (callback == null) { callback = function(error) {}; } for (file of Array.from(resources || [])) { - for (let dir of Array.from(__guard__(file != null ? file.path : undefined, x => x.split('/')))) { + for (const dir of Array.from(__guard__(file != null ? file.path : undefined, x => x.split('/')))) { if (dir === '..') { return callback(new Error("relative path in resource file list")); } diff --git a/app/coffee/ResourceWriter.js b/app/coffee/ResourceWriter.js index 0044ad93..028fc533 100644 --- a/app/coffee/ResourceWriter.js +++ b/app/coffee/ResourceWriter.js @@ -1,3 +1,12 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -105,7 +114,7 @@ module.exports = (ResourceWriter = { if (error != null) { return callback(error); } const jobs = []; - for (let file of Array.from(outputFiles || [])) { + for (const file of Array.from(outputFiles || [])) { (function(file) { const { path } = file; let should_delete = true; @@ -182,7 +191,7 @@ module.exports = (ResourceWriter = { logger.err({err, project_id, path, resource_url:resource.url, modified:resource.modified}, "error downloading file for resources"); } return callback(); - }); //try and continue compiling even if http resource can not be downloaded at this time + }); // try and continue compiling even if http resource can not be downloaded at this time } else { const process = require("process"); fs.writeFile(path, resource.content, callback); diff --git a/app/coffee/SafeReader.js b/app/coffee/SafeReader.js index f1a63494..2fd599b3 100644 --- a/app/coffee/SafeReader.js +++ b/app/coffee/SafeReader.js @@ -1,3 +1,10 @@ +/* eslint-disable + handle-callback-err, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/StaticServerForbidSymlinks.js b/app/coffee/StaticServerForbidSymlinks.js index 90a8879d..8ac3e489 100644 --- a/app/coffee/StaticServerForbidSymlinks.js +++ b/app/coffee/StaticServerForbidSymlinks.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + no-cond-assign, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -28,7 +36,7 @@ module.exports = (ForbidSymlinks = function(staticFn, root, options) { return res.sendStatus(404); } // check that the file does not use a relative path - for (let dir of Array.from(file.split('/'))) { + for (const dir of Array.from(file.split('/'))) { if (dir === '..') { logger.warn({path}, "attempt to use a relative path"); return res.sendStatus(404); diff --git a/app/coffee/TikzManager.js b/app/coffee/TikzManager.js index fb526447..9fa4a936 100644 --- a/app/coffee/TikzManager.js +++ b/app/coffee/TikzManager.js @@ -1,3 +1,9 @@ +/* eslint-disable + handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -21,7 +27,7 @@ module.exports = (TikzManager = { checkMainFile(compileDir, mainFile, resources, callback) { // if there's already an output.tex file, we don't want to touch it if (callback == null) { callback = function(error, needsMainFile) {}; } - for (let resource of Array.from(resources)) { + for (const resource of Array.from(resources)) { if (resource.path === "output.tex") { logger.log({compileDir, mainFile}, "output.tex already in resources"); return callback(null, false); diff --git a/app/coffee/UrlCache.js b/app/coffee/UrlCache.js index 9a199688..ade815b2 100644 --- a/app/coffee/UrlCache.js +++ b/app/coffee/UrlCache.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/app/coffee/UrlFetcher.js b/app/coffee/UrlFetcher.js index ea209560..fec397c1 100644 --- a/app/coffee/UrlFetcher.js +++ b/app/coffee/UrlFetcher.js @@ -1,3 +1,11 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/app/coffee/db.js b/app/coffee/db.js index 385ad8d3..c5dd9800 100644 --- a/app/coffee/db.js +++ b/app/coffee/db.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-console, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns From 4576ef54fbbaf3e2a13da28bc36ce1d05fd64e99 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:14:34 +0100 Subject: [PATCH 473/709] decaffeinate: rename app/coffee dir to app/js --- app/{coffee => js}/CommandRunner.js | 0 app/{coffee => js}/CompileController.js | 0 app/{coffee => js}/CompileManager.js | 0 app/{coffee => js}/ContentTypeMapper.js | 0 app/{coffee => js}/DbQueue.js | 0 app/{coffee => js}/DockerLockManager.js | 0 app/{coffee => js}/DockerRunner.js | 0 app/{coffee => js}/DraftModeManager.js | 0 app/{coffee => js}/Errors.js | 0 app/{coffee => js}/LatexRunner.js | 0 app/{coffee => js}/LocalCommandRunner.js | 0 app/{coffee => js}/LockManager.js | 0 app/{coffee => js}/Metrics.js | 0 app/{coffee => js}/OutputCacheManager.js | 0 app/{coffee => js}/OutputFileFinder.js | 0 app/{coffee => js}/OutputFileOptimiser.js | 0 app/{coffee => js}/ProjectPersistenceManager.js | 0 app/{coffee => js}/RequestParser.js | 0 app/{coffee => js}/ResourceStateManager.js | 0 app/{coffee => js}/ResourceWriter.js | 0 app/{coffee => js}/SafeReader.js | 0 app/{coffee => js}/StaticServerForbidSymlinks.js | 0 app/{coffee => js}/TikzManager.js | 0 app/{coffee => js}/UrlCache.js | 0 app/{coffee => js}/UrlFetcher.js | 0 app/{coffee => js}/db.js | 0 26 files changed, 0 insertions(+), 0 deletions(-) rename app/{coffee => js}/CommandRunner.js (100%) rename app/{coffee => js}/CompileController.js (100%) rename app/{coffee => js}/CompileManager.js (100%) rename app/{coffee => js}/ContentTypeMapper.js (100%) rename app/{coffee => js}/DbQueue.js (100%) rename app/{coffee => js}/DockerLockManager.js (100%) rename app/{coffee => js}/DockerRunner.js (100%) rename app/{coffee => js}/DraftModeManager.js (100%) rename app/{coffee => js}/Errors.js (100%) rename app/{coffee => js}/LatexRunner.js (100%) rename app/{coffee => js}/LocalCommandRunner.js (100%) rename app/{coffee => js}/LockManager.js (100%) rename app/{coffee => js}/Metrics.js (100%) rename app/{coffee => js}/OutputCacheManager.js (100%) rename app/{coffee => js}/OutputFileFinder.js (100%) rename app/{coffee => js}/OutputFileOptimiser.js (100%) rename app/{coffee => js}/ProjectPersistenceManager.js (100%) rename app/{coffee => js}/RequestParser.js (100%) rename app/{coffee => js}/ResourceStateManager.js (100%) rename app/{coffee => js}/ResourceWriter.js (100%) rename app/{coffee => js}/SafeReader.js (100%) rename app/{coffee => js}/StaticServerForbidSymlinks.js (100%) rename app/{coffee => js}/TikzManager.js (100%) rename app/{coffee => js}/UrlCache.js (100%) rename app/{coffee => js}/UrlFetcher.js (100%) rename app/{coffee => js}/db.js (100%) diff --git a/app/coffee/CommandRunner.js b/app/js/CommandRunner.js similarity index 100% rename from app/coffee/CommandRunner.js rename to app/js/CommandRunner.js diff --git a/app/coffee/CompileController.js b/app/js/CompileController.js similarity index 100% rename from app/coffee/CompileController.js rename to app/js/CompileController.js diff --git a/app/coffee/CompileManager.js b/app/js/CompileManager.js similarity index 100% rename from app/coffee/CompileManager.js rename to app/js/CompileManager.js diff --git a/app/coffee/ContentTypeMapper.js b/app/js/ContentTypeMapper.js similarity index 100% rename from app/coffee/ContentTypeMapper.js rename to app/js/ContentTypeMapper.js diff --git a/app/coffee/DbQueue.js b/app/js/DbQueue.js similarity index 100% rename from app/coffee/DbQueue.js rename to app/js/DbQueue.js diff --git a/app/coffee/DockerLockManager.js b/app/js/DockerLockManager.js similarity index 100% rename from app/coffee/DockerLockManager.js rename to app/js/DockerLockManager.js diff --git a/app/coffee/DockerRunner.js b/app/js/DockerRunner.js similarity index 100% rename from app/coffee/DockerRunner.js rename to app/js/DockerRunner.js diff --git a/app/coffee/DraftModeManager.js b/app/js/DraftModeManager.js similarity index 100% rename from app/coffee/DraftModeManager.js rename to app/js/DraftModeManager.js diff --git a/app/coffee/Errors.js b/app/js/Errors.js similarity index 100% rename from app/coffee/Errors.js rename to app/js/Errors.js diff --git a/app/coffee/LatexRunner.js b/app/js/LatexRunner.js similarity index 100% rename from app/coffee/LatexRunner.js rename to app/js/LatexRunner.js diff --git a/app/coffee/LocalCommandRunner.js b/app/js/LocalCommandRunner.js similarity index 100% rename from app/coffee/LocalCommandRunner.js rename to app/js/LocalCommandRunner.js diff --git a/app/coffee/LockManager.js b/app/js/LockManager.js similarity index 100% rename from app/coffee/LockManager.js rename to app/js/LockManager.js diff --git a/app/coffee/Metrics.js b/app/js/Metrics.js similarity index 100% rename from app/coffee/Metrics.js rename to app/js/Metrics.js diff --git a/app/coffee/OutputCacheManager.js b/app/js/OutputCacheManager.js similarity index 100% rename from app/coffee/OutputCacheManager.js rename to app/js/OutputCacheManager.js diff --git a/app/coffee/OutputFileFinder.js b/app/js/OutputFileFinder.js similarity index 100% rename from app/coffee/OutputFileFinder.js rename to app/js/OutputFileFinder.js diff --git a/app/coffee/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js similarity index 100% rename from app/coffee/OutputFileOptimiser.js rename to app/js/OutputFileOptimiser.js diff --git a/app/coffee/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js similarity index 100% rename from app/coffee/ProjectPersistenceManager.js rename to app/js/ProjectPersistenceManager.js diff --git a/app/coffee/RequestParser.js b/app/js/RequestParser.js similarity index 100% rename from app/coffee/RequestParser.js rename to app/js/RequestParser.js diff --git a/app/coffee/ResourceStateManager.js b/app/js/ResourceStateManager.js similarity index 100% rename from app/coffee/ResourceStateManager.js rename to app/js/ResourceStateManager.js diff --git a/app/coffee/ResourceWriter.js b/app/js/ResourceWriter.js similarity index 100% rename from app/coffee/ResourceWriter.js rename to app/js/ResourceWriter.js diff --git a/app/coffee/SafeReader.js b/app/js/SafeReader.js similarity index 100% rename from app/coffee/SafeReader.js rename to app/js/SafeReader.js diff --git a/app/coffee/StaticServerForbidSymlinks.js b/app/js/StaticServerForbidSymlinks.js similarity index 100% rename from app/coffee/StaticServerForbidSymlinks.js rename to app/js/StaticServerForbidSymlinks.js diff --git a/app/coffee/TikzManager.js b/app/js/TikzManager.js similarity index 100% rename from app/coffee/TikzManager.js rename to app/js/TikzManager.js diff --git a/app/coffee/UrlCache.js b/app/js/UrlCache.js similarity index 100% rename from app/coffee/UrlCache.js rename to app/js/UrlCache.js diff --git a/app/coffee/UrlFetcher.js b/app/js/UrlFetcher.js similarity index 100% rename from app/coffee/UrlFetcher.js rename to app/js/UrlFetcher.js diff --git a/app/coffee/db.js b/app/js/db.js similarity index 100% rename from app/coffee/db.js rename to app/js/db.js From cffbd4e9efbdab23534f9c3e96ea7a808417fa5d Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:14:37 +0100 Subject: [PATCH 474/709] prettier: convert app/js decaffeinated files to Prettier format --- app/js/CommandRunner.js | 18 +- app/js/CompileController.js | 356 ++++---- app/js/CompileManager.js | 1126 ++++++++++++++++---------- app/js/ContentTypeMapper.js | 53 +- app/js/DbQueue.js | 18 +- app/js/DockerLockManager.js | 166 ++-- app/js/DockerRunner.js | 1117 ++++++++++++++----------- app/js/DraftModeManager.js | 73 +- app/js/Errors.js | 48 +- app/js/LatexRunner.js | 301 ++++--- app/js/LocalCommandRunner.js | 117 +-- app/js/LockManager.js | 100 ++- app/js/Metrics.js | 3 +- app/js/OutputCacheManager.js | 602 ++++++++------ app/js/OutputFileFinder.js | 155 ++-- app/js/OutputFileOptimiser.js | 152 ++-- app/js/ProjectPersistenceManager.js | 242 +++--- app/js/RequestParser.js | 372 +++++---- app/js/ResourceStateManager.js | 224 ++--- app/js/ResourceWriter.js | 511 +++++++----- app/js/SafeReader.js | 73 +- app/js/StaticServerForbidSymlinks.js | 128 +-- app/js/TikzManager.js | 118 ++- app/js/UrlCache.js | 420 ++++++---- app/js/UrlFetcher.js | 166 ++-- app/js/db.js | 89 +- 26 files changed, 3995 insertions(+), 2753 deletions(-) diff --git a/app/js/CommandRunner.js b/app/js/CommandRunner.js index 1c46ee95..8e07dacf 100644 --- a/app/js/CommandRunner.js +++ b/app/js/CommandRunner.js @@ -5,16 +5,16 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let commandRunnerPath; -const Settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); +let commandRunnerPath +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') if ((Settings.clsi != null ? Settings.clsi.dockerRunner : undefined) === true) { - commandRunnerPath = "./DockerRunner"; -} else { - commandRunnerPath = "./LocalCommandRunner"; + commandRunnerPath = './DockerRunner' +} else { + commandRunnerPath = './LocalCommandRunner' } -logger.info({commandRunnerPath}, "selecting command runner for clsi"); -const CommandRunner = require(commandRunnerPath); +logger.info({ commandRunnerPath }, 'selecting command runner for clsi') +const CommandRunner = require(commandRunnerPath) -module.exports = CommandRunner; +module.exports = CommandRunner diff --git a/app/js/CompileController.js b/app/js/CompileController.js index 60925fc1..e146b62c 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -12,159 +12,227 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let CompileController; -const RequestParser = require("./RequestParser"); -const CompileManager = require("./CompileManager"); -const Settings = require("settings-sharelatex"); -const Metrics = require("./Metrics"); -const ProjectPersistenceManager = require("./ProjectPersistenceManager"); -const logger = require("logger-sharelatex"); -const Errors = require("./Errors"); +let CompileController +const RequestParser = require('./RequestParser') +const CompileManager = require('./CompileManager') +const Settings = require('settings-sharelatex') +const Metrics = require('./Metrics') +const ProjectPersistenceManager = require('./ProjectPersistenceManager') +const logger = require('logger-sharelatex') +const Errors = require('./Errors') -module.exports = (CompileController = { - compile(req, res, next) { - if (next == null) { next = function(error) {}; } - const timer = new Metrics.Timer("compile-request"); - return RequestParser.parse(req.body, function(error, request) { - if (error != null) { return next(error); } - request.project_id = req.params.project_id; - if (req.params.user_id != null) { request.user_id = req.params.user_id; } - return ProjectPersistenceManager.markProjectAsJustAccessed(request.project_id, function(error) { - if (error != null) { return next(error); } - return CompileManager.doCompileWithLock(request, function(error, outputFiles) { - let code, status; - if (outputFiles == null) { outputFiles = []; } - if (error instanceof Errors.AlreadyCompilingError) { - code = 423; // Http 423 Locked - status = "compile-in-progress"; - } else if (error instanceof Errors.FilesOutOfSyncError) { - code = 409; // Http 409 Conflict - status = "retry"; - } else if (error != null ? error.terminated : undefined) { - status = "terminated"; - } else if (error != null ? error.validate : undefined) { - status = `validation-${error.validate}`; - } else if (error != null ? error.timedout : undefined) { - status = "timedout"; - logger.log({err: error, project_id: request.project_id}, "timeout running compile"); - } else if (error != null) { - status = "error"; - code = 500; - logger.warn({err: error, project_id: request.project_id}, "error running compile"); - } else { - let file; - status = "failure"; - for (file of Array.from(outputFiles)) { - if (file.path != null ? file.path.match(/output\.pdf$/) : undefined) { - status = "success"; - } - } +module.exports = CompileController = { + compile(req, res, next) { + if (next == null) { + next = function(error) {} + } + const timer = new Metrics.Timer('compile-request') + return RequestParser.parse(req.body, function(error, request) { + if (error != null) { + return next(error) + } + request.project_id = req.params.project_id + if (req.params.user_id != null) { + request.user_id = req.params.user_id + } + return ProjectPersistenceManager.markProjectAsJustAccessed( + request.project_id, + function(error) { + if (error != null) { + return next(error) + } + return CompileManager.doCompileWithLock(request, function( + error, + outputFiles + ) { + let code, status + if (outputFiles == null) { + outputFiles = [] + } + if (error instanceof Errors.AlreadyCompilingError) { + code = 423 // Http 423 Locked + status = 'compile-in-progress' + } else if (error instanceof Errors.FilesOutOfSyncError) { + code = 409 // Http 409 Conflict + status = 'retry' + } else if (error != null ? error.terminated : undefined) { + status = 'terminated' + } else if (error != null ? error.validate : undefined) { + status = `validation-${error.validate}` + } else if (error != null ? error.timedout : undefined) { + status = 'timedout' + logger.log( + { err: error, project_id: request.project_id }, + 'timeout running compile' + ) + } else if (error != null) { + status = 'error' + code = 500 + logger.warn( + { err: error, project_id: request.project_id }, + 'error running compile' + ) + } else { + let file + status = 'failure' + for (file of Array.from(outputFiles)) { + if ( + file.path != null + ? file.path.match(/output\.pdf$/) + : undefined + ) { + status = 'success' + } + } - if (status === "failure") { - logger.warn({project_id: request.project_id, outputFiles}, "project failed to compile successfully, no output.pdf generated"); - } + if (status === 'failure') { + logger.warn( + { project_id: request.project_id, outputFiles }, + 'project failed to compile successfully, no output.pdf generated' + ) + } - // log an error if any core files are found - for (file of Array.from(outputFiles)) { - if (file.path === "core") { - logger.error({project_id:request.project_id, req, outputFiles}, "core file found in output"); - } - } - } + // log an error if any core files are found + for (file of Array.from(outputFiles)) { + if (file.path === 'core') { + logger.error( + { project_id: request.project_id, req, outputFiles }, + 'core file found in output' + ) + } + } + } - if (error != null) { - outputFiles = error.outputFiles || []; - } + if (error != null) { + outputFiles = error.outputFiles || [] + } - timer.done(); - return res.status(code || 200).send({ - compile: { - status, - error: (error != null ? error.message : undefined) || error, - outputFiles: outputFiles.map(file => - ({ - url: - `${Settings.apis.clsi.url}/project/${request.project_id}` + - ((request.user_id != null) ? `/user/${request.user_id}` : "") + - ((file.build != null) ? `/build/${file.build}` : "") + - `/output/${file.path}`, - path: file.path, - type: file.type, - build: file.build - }) - ) - } - }); - }); - }); - }); - }, + timer.done() + return res.status(code || 200).send({ + compile: { + status, + error: (error != null ? error.message : undefined) || error, + outputFiles: outputFiles.map(file => ({ + url: + `${Settings.apis.clsi.url}/project/${request.project_id}` + + (request.user_id != null + ? `/user/${request.user_id}` + : '') + + (file.build != null ? `/build/${file.build}` : '') + + `/output/${file.path}`, + path: file.path, + type: file.type, + build: file.build + })) + } + }) + }) + } + ) + }) + }, - stopCompile(req, res, next) { - const {project_id, user_id} = req.params; - return CompileManager.stopCompile(project_id, user_id, function(error) { - if (error != null) { return next(error); } - return res.sendStatus(204); - }); - }, + stopCompile(req, res, next) { + const { project_id, user_id } = req.params + return CompileManager.stopCompile(project_id, user_id, function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + }) + }, - clearCache(req, res, next) { - if (next == null) { next = function(error) {}; } - return ProjectPersistenceManager.clearProject(req.params.project_id, req.params.user_id, function(error) { - if (error != null) { return next(error); } - return res.sendStatus(204); - }); - }, // No content + clearCache(req, res, next) { + if (next == null) { + next = function(error) {} + } + return ProjectPersistenceManager.clearProject( + req.params.project_id, + req.params.user_id, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + }, // No content - syncFromCode(req, res, next) { - if (next == null) { next = function(error) {}; } - const { file } = req.query; - const line = parseInt(req.query.line, 10); - const column = parseInt(req.query.column, 10); - const { project_id } = req.params; - const { user_id } = req.params; - return CompileManager.syncFromCode(project_id, user_id, file, line, column, function(error, pdfPositions) { - if (error != null) { return next(error); } - return res.json({ - pdf: pdfPositions - }); - }); - }, + syncFromCode(req, res, next) { + if (next == null) { + next = function(error) {} + } + const { file } = req.query + const line = parseInt(req.query.line, 10) + const column = parseInt(req.query.column, 10) + const { project_id } = req.params + const { user_id } = req.params + return CompileManager.syncFromCode( + project_id, + user_id, + file, + line, + column, + function(error, pdfPositions) { + if (error != null) { + return next(error) + } + return res.json({ + pdf: pdfPositions + }) + } + ) + }, - syncFromPdf(req, res, next) { - if (next == null) { next = function(error) {}; } - const page = parseInt(req.query.page, 10); - const h = parseFloat(req.query.h); - const v = parseFloat(req.query.v); - const { project_id } = req.params; - const { user_id } = req.params; - return CompileManager.syncFromPdf(project_id, user_id, page, h, v, function(error, codePositions) { - if (error != null) { return next(error); } - return res.json({ - code: codePositions - }); - }); - }, + syncFromPdf(req, res, next) { + if (next == null) { + next = function(error) {} + } + const page = parseInt(req.query.page, 10) + const h = parseFloat(req.query.h) + const v = parseFloat(req.query.v) + const { project_id } = req.params + const { user_id } = req.params + return CompileManager.syncFromPdf(project_id, user_id, page, h, v, function( + error, + codePositions + ) { + if (error != null) { + return next(error) + } + return res.json({ + code: codePositions + }) + }) + }, - wordcount(req, res, next) { - if (next == null) { next = function(error) {}; } - const file = req.query.file || "main.tex"; - const { project_id } = req.params; - const { user_id } = req.params; - const { image } = req.query; - logger.log({image, file, project_id}, "word count request"); + wordcount(req, res, next) { + if (next == null) { + next = function(error) {} + } + const file = req.query.file || 'main.tex' + const { project_id } = req.params + const { user_id } = req.params + const { image } = req.query + logger.log({ image, file, project_id }, 'word count request') - return CompileManager.wordcount(project_id, user_id, file, image, function(error, result) { - if (error != null) { return next(error); } - return res.json({ - texcount: result - }); - }); - }, - - status(req, res, next ){ - if (next == null) { next = function(error){}; } - return res.send("OK"); - } -}); + return CompileManager.wordcount(project_id, user_id, file, image, function( + error, + result + ) { + if (error != null) { + return next(error) + } + return res.json({ + texcount: result + }) + }) + }, + status(req, res, next) { + if (next == null) { + next = function(error) {} + } + return res.send('OK') + } +} diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 76fb8b04..3bf54bc7 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -15,449 +15,691 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let CompileManager; -const ResourceWriter = require("./ResourceWriter"); -const LatexRunner = require("./LatexRunner"); -const OutputFileFinder = require("./OutputFileFinder"); -const OutputCacheManager = require("./OutputCacheManager"); -const Settings = require("settings-sharelatex"); -const Path = require("path"); -const logger = require("logger-sharelatex"); -const Metrics = require("./Metrics"); -const child_process = require("child_process"); -const DraftModeManager = require("./DraftModeManager"); -const TikzManager = require("./TikzManager"); -const LockManager = require("./LockManager"); -const fs = require("fs"); -const fse = require("fs-extra"); -const os = require("os"); -const async = require("async"); -const Errors = require('./Errors'); -const CommandRunner = require("./CommandRunner"); +let CompileManager +const ResourceWriter = require('./ResourceWriter') +const LatexRunner = require('./LatexRunner') +const OutputFileFinder = require('./OutputFileFinder') +const OutputCacheManager = require('./OutputCacheManager') +const Settings = require('settings-sharelatex') +const Path = require('path') +const logger = require('logger-sharelatex') +const Metrics = require('./Metrics') +const child_process = require('child_process') +const DraftModeManager = require('./DraftModeManager') +const TikzManager = require('./TikzManager') +const LockManager = require('./LockManager') +const fs = require('fs') +const fse = require('fs-extra') +const os = require('os') +const async = require('async') +const Errors = require('./Errors') +const CommandRunner = require('./CommandRunner') const getCompileName = function(project_id, user_id) { - if (user_id != null) { return `${project_id}-${user_id}`; } else { return project_id; } -}; - -const getCompileDir = (project_id, user_id) => Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id)); - -module.exports = (CompileManager = { - - doCompileWithLock(request, callback) { - if (callback == null) { callback = function(error, outputFiles) {}; } - const compileDir = getCompileDir(request.project_id, request.user_id); - const lockFile = Path.join(compileDir, ".project-lock"); - // use a .project-lock file in the compile directory to prevent - // simultaneous compiles - return fse.ensureDir(compileDir, function(error) { - if (error != null) { return callback(error); } - return LockManager.runWithLock(lockFile, releaseLock => CompileManager.doCompile(request, releaseLock) - , callback); - }); - }, - - doCompile(request, callback) { - if (callback == null) { callback = function(error, outputFiles) {}; } - const compileDir = getCompileDir(request.project_id, request.user_id); - let timer = new Metrics.Timer("write-to-disk"); - logger.log({project_id: request.project_id, user_id: request.user_id}, "syncing resources to disk"); - return ResourceWriter.syncResourcesToDisk(request, compileDir, function(error, resourceList) { - // NOTE: resourceList is insecure, it should only be used to exclude files from the output list - if ((error != null) && error instanceof Errors.FilesOutOfSyncError) { - logger.warn({project_id: request.project_id, user_id: request.user_id}, "files out of sync, please retry"); - return callback(error); - } else if (error != null) { - logger.err({err:error, project_id: request.project_id, user_id: request.user_id}, "error writing resources to disk"); - return callback(error); - } - logger.log({project_id: request.project_id, user_id: request.user_id, time_taken: Date.now() - timer.start}, "written files to disk"); - timer.done(); - - const injectDraftModeIfRequired = function(callback) { - if (request.draft) { - return DraftModeManager.injectDraftMode(Path.join(compileDir, request.rootResourcePath), callback); - } else { - return callback(); - } - }; - - const createTikzFileIfRequired = callback => - TikzManager.checkMainFile(compileDir, request.rootResourcePath, resourceList, function(error, needsMainFile) { - if (error != null) { return callback(error); } - if (needsMainFile) { - return TikzManager.injectOutputFile(compileDir, request.rootResourcePath, callback); - } else { - return callback(); - } - }) - ; - - // set up environment variables for chktex - const env = {}; - // only run chktex on LaTeX files (not knitr .Rtex files or any others) - const isLaTeXFile = request.rootResourcePath != null ? request.rootResourcePath.match(/\.tex$/i) : undefined; - if ((request.check != null) && isLaTeXFile) { - env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16'; - env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000'; - if (request.check === 'error') { - env.CHKTEX_EXIT_ON_ERROR = 1; - } - if (request.check === 'validate') { - env.CHKTEX_VALIDATE = 1; - } - } - - // apply a series of file modifications/creations for draft mode and tikz - return async.series([injectDraftModeIfRequired, createTikzFileIfRequired], function(error) { - if (error != null) { return callback(error); } - timer = new Metrics.Timer("run-compile"); - // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) - let tag = __guard__(__guard__(request.imageName != null ? request.imageName.match(/:(.*)/) : undefined, x1 => x1[1]), x => x.replace(/\./g,'-')) || "default"; - if (!request.project_id.match(/^[0-9a-f]{24}$/)) { tag = "other"; } // exclude smoke test - Metrics.inc("compiles"); - Metrics.inc(`compiles-with-image.${tag}`); - const compileName = getCompileName(request.project_id, request.user_id); - return LatexRunner.runLatex(compileName, { - directory: compileDir, - mainFile: request.rootResourcePath, - compiler: request.compiler, - timeout: request.timeout, - image: request.imageName, - flags: request.flags, - environment: env - }, function(error, output, stats, timings) { - // request was for validation only - let metric_key, metric_value; - if (request.check === "validate") { - const result = (error != null ? error.code : undefined) ? "fail" : "pass"; - error = new Error("validation"); - error.validate = result; - } - // request was for compile, and failed on validation - if ((request.check === "error") && ((error != null ? error.message : undefined) === 'exited')) { - error = new Error("compilation"); - error.validate = "fail"; - } - // compile was killed by user, was a validation, or a compile which failed validation - if ((error != null ? error.terminated : undefined) || (error != null ? error.validate : undefined) || (error != null ? error.timedout : undefined)) { - OutputFileFinder.findOutputFiles(resourceList, compileDir, function(err, outputFiles) { - if (err != null) { return callback(err); } - error.outputFiles = outputFiles; // return output files so user can check logs - return callback(error); - }); - return; - } - // compile completed normally - if (error != null) { return callback(error); } - Metrics.inc("compiles-succeeded"); - const object = stats || {}; - for (metric_key in object) { - metric_value = object[metric_key]; - Metrics.count(metric_key, metric_value); - } - const object1 = timings || {}; - for (metric_key in object1) { - metric_value = object1[metric_key]; - Metrics.timing(metric_key, metric_value); - } - const loadavg = typeof os.loadavg === 'function' ? os.loadavg() : undefined; - if (loadavg != null) { Metrics.gauge("load-avg", loadavg[0]); } - const ts = timer.done(); - logger.log({project_id: request.project_id, user_id: request.user_id, time_taken: ts, stats, timings, loadavg}, "done compile"); - if ((stats != null ? stats["latex-runs"] : undefined) > 0) { - Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"]); - } - if (((stats != null ? stats["latex-runs"] : undefined) > 0) && ((timings != null ? timings["cpu-time"] : undefined) > 0)) { - Metrics.timing("run-compile-cpu-time-per-pass", timings["cpu-time"] / stats["latex-runs"]); - } - - return OutputFileFinder.findOutputFiles(resourceList, compileDir, function(error, outputFiles) { - if (error != null) { return callback(error); } - return OutputCacheManager.saveOutputFiles(outputFiles, compileDir, (error, newOutputFiles) => callback(null, newOutputFiles)); - }); - }); - }); - }); - }, - - stopCompile(project_id, user_id, callback) { - if (callback == null) { callback = function(error) {}; } - const compileName = getCompileName(project_id, user_id); - return LatexRunner.killLatex(compileName, callback); - }, - - clearProject(project_id, user_id, _callback) { - if (_callback == null) { _callback = function(error) {}; } - const callback = function(error) { - _callback(error); - return _callback = function() {}; - }; - - const compileDir = getCompileDir(project_id, user_id); - - return CompileManager._checkDirectory(compileDir, function(err, exists) { - if (err != null) { return callback(err); } - if (!exists) { return callback(); } // skip removal if no directory present - - const proc = child_process.spawn("rm", ["-r", compileDir]); - - proc.on("error", callback); - - let stderr = ""; - proc.stderr.on("data", chunk => stderr += chunk.toString()); - - return proc.on("close", function(code) { - if (code === 0) { - return callback(null); - } else { - return callback(new Error(`rm -r ${compileDir} failed: ${stderr}`)); - } - }); - }); - }, - - _findAllDirs(callback) { - if (callback == null) { callback = function(error, allDirs) {}; } - const root = Settings.path.compilesDir; - return fs.readdir(root, function(err, files) { - if (err != null) { return callback(err); } - const allDirs = (Array.from(files).map((file) => Path.join(root, file))); - return callback(null, allDirs); - }); - }, - - clearExpiredProjects(max_cache_age_ms, callback) { - if (callback == null) { callback = function(error) {}; } - const now = Date.now(); - // action for each directory - const expireIfNeeded = (checkDir, cb) => - fs.stat(checkDir, function(err, stats) { - if (err != null) { return cb(); } // ignore errors checking directory - const age = now - stats.mtime; - const hasExpired = (age > max_cache_age_ms); - if (hasExpired) { return fse.remove(checkDir, cb); } else { return cb(); } - }) - ; - // iterate over all project directories - return CompileManager._findAllDirs(function(error, allDirs) { - if (error != null) { return callback(); } - return async.eachSeries(allDirs, expireIfNeeded, callback); - }); - }, - - _checkDirectory(compileDir, callback) { - if (callback == null) { callback = function(error, exists) {}; } - return fs.lstat(compileDir, function(err, stats) { - if ((err != null ? err.code : undefined) === 'ENOENT') { - return callback(null, false); // directory does not exist - } else if (err != null) { - logger.err({dir: compileDir, err}, "error on stat of project directory for removal"); - return callback(err); - } else if (!(stats != null ? stats.isDirectory() : undefined)) { - logger.err({dir: compileDir, stats}, "bad project directory for removal"); - return callback(new Error("project directory is not directory")); - } else { - return callback(null, true); - } - }); - }, // directory exists - - syncFromCode(project_id, user_id, file_name, line, column, callback) { - // If LaTeX was run in a virtual environment, the file path that synctex expects - // might not match the file path on the host. The .synctex.gz file however, will be accessed - // wherever it is on the host. - if (callback == null) { callback = function(error, pdfPositions) {}; } - const compileName = getCompileName(project_id, user_id); - const base_dir = Settings.path.synctexBaseDir(compileName); - const file_path = base_dir + "/" + file_name; - const compileDir = getCompileDir(project_id, user_id); - const synctex_path = `${base_dir}/output.pdf`; - const command = ["code", synctex_path, file_path, line, column]; - return fse.ensureDir(compileDir, function(error) { - if (error != null) { - logger.err({error, project_id, user_id, file_name}, "error ensuring dir for sync from code"); - return callback(error); - } - return CompileManager._runSynctex(project_id, user_id, command, function(error, stdout) { - if (error != null) { return callback(error); } - logger.log({project_id, user_id, file_name, line, column, command, stdout}, "synctex code output"); - return callback(null, CompileManager._parseSynctexFromCodeOutput(stdout)); - }); - }); - }, - - syncFromPdf(project_id, user_id, page, h, v, callback) { - if (callback == null) { callback = function(error, filePositions) {}; } - const compileName = getCompileName(project_id, user_id); - const compileDir = getCompileDir(project_id, user_id); - const base_dir = Settings.path.synctexBaseDir(compileName); - const synctex_path = `${base_dir}/output.pdf`; - const command = ["pdf", synctex_path, page, h, v]; - return fse.ensureDir(compileDir, function(error) { - if (error != null) { - logger.err({error, project_id, user_id, file_name}, "error ensuring dir for sync to code"); - return callback(error); - } - return CompileManager._runSynctex(project_id, user_id, command, function(error, stdout) { - if (error != null) { return callback(error); } - logger.log({project_id, user_id, page, h, v, stdout}, "synctex pdf output"); - return callback(null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)); - }); - }); - }, - - _checkFileExists(path, callback) { - if (callback == null) { callback = function(error) {}; } - const synctexDir = Path.dirname(path); - const synctexFile = Path.join(synctexDir, "output.synctex.gz"); - return fs.stat(synctexDir, function(error, stats) { - if ((error != null ? error.code : undefined) === 'ENOENT') { - return callback(new Errors.NotFoundError("called synctex with no output directory")); - } - if (error != null) { return callback(error); } - return fs.stat(synctexFile, function(error, stats) { - if ((error != null ? error.code : undefined) === 'ENOENT') { - return callback(new Errors.NotFoundError("called synctex with no output file")); - } - if (error != null) { return callback(error); } - if (!(stats != null ? stats.isFile() : undefined)) { return callback(new Error("not a file")); } - return callback(); - }); - }); - }, - - _runSynctex(project_id, user_id, command, callback) { - if (callback == null) { callback = function(error, stdout) {}; } - const seconds = 1000; - - command.unshift("/opt/synctex"); - - const directory = getCompileDir(project_id, user_id); - const timeout = 60 * 1000; // increased to allow for large projects - const compileName = getCompileName(project_id, user_id); - return CommandRunner.run(compileName, command, directory, Settings.clsi != null ? Settings.clsi.docker.image : undefined, timeout, {}, function(error, output) { - if (error != null) { - logger.err({err:error, command, project_id, user_id}, "error running synctex"); - return callback(error); - } - return callback(null, output.stdout); - }); - }, - - _parseSynctexFromCodeOutput(output) { - const results = []; - for (const line of Array.from(output.split("\n"))) { - const [node, page, h, v, width, height] = Array.from(line.split("\t")); - if (node === "NODE") { - results.push({ - page: parseInt(page, 10), - h: parseFloat(h), - v: parseFloat(v), - height: parseFloat(height), - width: parseFloat(width) - }); - } - } - return results; - }, - - _parseSynctexFromPdfOutput(output, base_dir) { - const results = []; - for (let line of Array.from(output.split("\n"))) { - let column, file_path, node; - [node, file_path, line, column] = Array.from(line.split("\t")); - if (node === "NODE") { - const file = file_path.slice(base_dir.length + 1); - results.push({ - file, - line: parseInt(line, 10), - column: parseInt(column, 10) - }); - } - } - return results; - }, - - - wordcount(project_id, user_id, file_name, image, callback) { - if (callback == null) { callback = function(error, pdfPositions) {}; } - logger.log({project_id, user_id, file_name, image}, "running wordcount"); - const file_path = `$COMPILE_DIR/${file_name}`; - const command = [ "texcount", '-nocol', '-inc', file_path, `-out=${file_path}.wc`]; - const compileDir = getCompileDir(project_id, user_id); - const timeout = 60 * 1000; - const compileName = getCompileName(project_id, user_id); - return fse.ensureDir(compileDir, function(error) { - if (error != null) { - logger.err({error, project_id, user_id, file_name}, "error ensuring dir for sync from code"); - return callback(error); - } - return CommandRunner.run(compileName, command, compileDir, image, timeout, {}, function(error) { - if (error != null) { return callback(error); } - return fs.readFile(compileDir + "/" + file_name + ".wc", "utf-8", function(err, stdout) { - if (err != null) { - // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored - logger.err({node_err:err, command, compileDir, project_id, user_id}, "error reading word count output"); - return callback(err); - } - const results = CompileManager._parseWordcountFromOutput(stdout); - logger.log({project_id, user_id, wordcount: results}, "word count results"); - return callback(null, results); - }); - }); - }); - }, - - _parseWordcountFromOutput(output) { - const results = { - encode: "", - textWords: 0, - headWords: 0, - outside: 0, - headers: 0, - elements: 0, - mathInline: 0, - mathDisplay: 0, - errors: 0, - messages: "" - }; - for (const line of Array.from(output.split("\n"))) { - const [data, info] = Array.from(line.split(":")); - if (data.indexOf("Encoding") > -1) { - results.encode = info.trim(); - } - if (data.indexOf("in text") > -1) { - results.textWords = parseInt(info, 10); - } - if (data.indexOf("in head") > -1) { - results.headWords = parseInt(info, 10); - } - if (data.indexOf("outside") > -1) { - results.outside = parseInt(info, 10); - } - if (data.indexOf("of head") > -1) { - results.headers = parseInt(info, 10); - } - if (data.indexOf("Number of floats/tables/figures") > -1) { - results.elements = parseInt(info, 10); - } - if (data.indexOf("Number of math inlines") > -1) { - results.mathInline = parseInt(info, 10); - } - if (data.indexOf("Number of math displayed") > -1) { - results.mathDisplay = parseInt(info, 10); - } - if (data === "(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; - } -}); + if (user_id != null) { + return `${project_id}-${user_id}` + } else { + return project_id + } +} + +const getCompileDir = (project_id, user_id) => + Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id)) + +module.exports = CompileManager = { + doCompileWithLock(request, callback) { + if (callback == null) { + callback = function(error, outputFiles) {} + } + const compileDir = getCompileDir(request.project_id, request.user_id) + const lockFile = Path.join(compileDir, '.project-lock') + // use a .project-lock file in the compile directory to prevent + // simultaneous compiles + return fse.ensureDir(compileDir, function(error) { + if (error != null) { + return callback(error) + } + return LockManager.runWithLock( + lockFile, + releaseLock => CompileManager.doCompile(request, releaseLock), + callback + ) + }) + }, + + doCompile(request, callback) { + if (callback == null) { + callback = function(error, outputFiles) {} + } + const compileDir = getCompileDir(request.project_id, request.user_id) + let timer = new Metrics.Timer('write-to-disk') + logger.log( + { project_id: request.project_id, user_id: request.user_id }, + 'syncing resources to disk' + ) + return ResourceWriter.syncResourcesToDisk(request, compileDir, function( + error, + resourceList + ) { + // NOTE: resourceList is insecure, it should only be used to exclude files from the output list + if (error != null && error instanceof Errors.FilesOutOfSyncError) { + logger.warn( + { project_id: request.project_id, user_id: request.user_id }, + 'files out of sync, please retry' + ) + return callback(error) + } else if (error != null) { + logger.err( + { + err: error, + project_id: request.project_id, + user_id: request.user_id + }, + 'error writing resources to disk' + ) + return callback(error) + } + logger.log( + { + project_id: request.project_id, + user_id: request.user_id, + time_taken: Date.now() - timer.start + }, + 'written files to disk' + ) + timer.done() + + const injectDraftModeIfRequired = function(callback) { + if (request.draft) { + return DraftModeManager.injectDraftMode( + Path.join(compileDir, request.rootResourcePath), + callback + ) + } else { + return callback() + } + } + + const createTikzFileIfRequired = callback => + TikzManager.checkMainFile( + compileDir, + request.rootResourcePath, + resourceList, + function(error, needsMainFile) { + if (error != null) { + return callback(error) + } + if (needsMainFile) { + return TikzManager.injectOutputFile( + compileDir, + request.rootResourcePath, + callback + ) + } else { + return callback() + } + } + ) + // set up environment variables for chktex + const env = {} + // only run chktex on LaTeX files (not knitr .Rtex files or any others) + const isLaTeXFile = + request.rootResourcePath != null + ? request.rootResourcePath.match(/\.tex$/i) + : undefined + if (request.check != null && isLaTeXFile) { + env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16' + env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000' + if (request.check === 'error') { + env.CHKTEX_EXIT_ON_ERROR = 1 + } + if (request.check === 'validate') { + env.CHKTEX_VALIDATE = 1 + } + } + + // apply a series of file modifications/creations for draft mode and tikz + return async.series( + [injectDraftModeIfRequired, createTikzFileIfRequired], + function(error) { + if (error != null) { + return callback(error) + } + timer = new Metrics.Timer('run-compile') + // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) + let tag = + __guard__( + __guard__( + request.imageName != null + ? request.imageName.match(/:(.*)/) + : undefined, + x1 => x1[1] + ), + x => x.replace(/\./g, '-') + ) || 'default' + if (!request.project_id.match(/^[0-9a-f]{24}$/)) { + tag = 'other' + } // exclude smoke test + Metrics.inc('compiles') + Metrics.inc(`compiles-with-image.${tag}`) + const compileName = getCompileName( + request.project_id, + request.user_id + ) + return LatexRunner.runLatex( + compileName, + { + directory: compileDir, + mainFile: request.rootResourcePath, + compiler: request.compiler, + timeout: request.timeout, + image: request.imageName, + flags: request.flags, + environment: env + }, + function(error, output, stats, timings) { + // request was for validation only + let metric_key, metric_value + if (request.check === 'validate') { + const result = (error != null + ? error.code + : undefined) + ? 'fail' + : 'pass' + error = new Error('validation') + error.validate = result + } + // request was for compile, and failed on validation + if ( + request.check === 'error' && + (error != null ? error.message : undefined) === 'exited' + ) { + error = new Error('compilation') + error.validate = 'fail' + } + // compile was killed by user, was a validation, or a compile which failed validation + if ( + (error != null ? error.terminated : undefined) || + (error != null ? error.validate : undefined) || + (error != null ? error.timedout : undefined) + ) { + OutputFileFinder.findOutputFiles( + resourceList, + compileDir, + function(err, outputFiles) { + if (err != null) { + return callback(err) + } + error.outputFiles = outputFiles // return output files so user can check logs + return callback(error) + } + ) + return + } + // compile completed normally + if (error != null) { + return callback(error) + } + Metrics.inc('compiles-succeeded') + const object = stats || {} + for (metric_key in object) { + metric_value = object[metric_key] + Metrics.count(metric_key, metric_value) + } + const object1 = timings || {} + for (metric_key in object1) { + metric_value = object1[metric_key] + Metrics.timing(metric_key, metric_value) + } + const loadavg = + typeof os.loadavg === 'function' ? os.loadavg() : undefined + if (loadavg != null) { + Metrics.gauge('load-avg', loadavg[0]) + } + const ts = timer.done() + logger.log( + { + project_id: request.project_id, + user_id: request.user_id, + time_taken: ts, + stats, + timings, + loadavg + }, + 'done compile' + ) + if ((stats != null ? stats['latex-runs'] : undefined) > 0) { + Metrics.timing('run-compile-per-pass', ts / stats['latex-runs']) + } + if ( + (stats != null ? stats['latex-runs'] : undefined) > 0 && + (timings != null ? timings['cpu-time'] : undefined) > 0 + ) { + Metrics.timing( + 'run-compile-cpu-time-per-pass', + timings['cpu-time'] / stats['latex-runs'] + ) + } + + return OutputFileFinder.findOutputFiles( + resourceList, + compileDir, + function(error, outputFiles) { + if (error != null) { + return callback(error) + } + return OutputCacheManager.saveOutputFiles( + outputFiles, + compileDir, + (error, newOutputFiles) => callback(null, newOutputFiles) + ) + } + ) + } + ) + } + ) + }) + }, + + stopCompile(project_id, user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + const compileName = getCompileName(project_id, user_id) + return LatexRunner.killLatex(compileName, callback) + }, + + clearProject(project_id, user_id, _callback) { + if (_callback == null) { + _callback = function(error) {} + } + const callback = function(error) { + _callback(error) + return (_callback = function() {}) + } + + const compileDir = getCompileDir(project_id, user_id) + + return CompileManager._checkDirectory(compileDir, function(err, exists) { + if (err != null) { + return callback(err) + } + if (!exists) { + return callback() + } // skip removal if no directory present + + const proc = child_process.spawn('rm', ['-r', compileDir]) + + proc.on('error', callback) + + let stderr = '' + proc.stderr.on('data', chunk => (stderr += chunk.toString())) + + return proc.on('close', function(code) { + if (code === 0) { + return callback(null) + } else { + return callback(new Error(`rm -r ${compileDir} failed: ${stderr}`)) + } + }) + }) + }, + + _findAllDirs(callback) { + if (callback == null) { + callback = function(error, allDirs) {} + } + const root = Settings.path.compilesDir + return fs.readdir(root, function(err, files) { + if (err != null) { + return callback(err) + } + const allDirs = Array.from(files).map(file => Path.join(root, file)) + return callback(null, allDirs) + }) + }, + + clearExpiredProjects(max_cache_age_ms, callback) { + if (callback == null) { + callback = function(error) {} + } + const now = Date.now() + // action for each directory + const expireIfNeeded = (checkDir, cb) => + fs.stat(checkDir, function(err, stats) { + if (err != null) { + return cb() + } // ignore errors checking directory + const age = now - stats.mtime + const hasExpired = age > max_cache_age_ms + if (hasExpired) { + return fse.remove(checkDir, cb) + } else { + return cb() + } + }) + // iterate over all project directories + return CompileManager._findAllDirs(function(error, allDirs) { + if (error != null) { + return callback() + } + return async.eachSeries(allDirs, expireIfNeeded, callback) + }) + }, + + _checkDirectory(compileDir, callback) { + if (callback == null) { + callback = function(error, exists) {} + } + return fs.lstat(compileDir, function(err, stats) { + if ((err != null ? err.code : undefined) === 'ENOENT') { + return callback(null, false) // directory does not exist + } else if (err != null) { + logger.err( + { dir: compileDir, err }, + 'error on stat of project directory for removal' + ) + return callback(err) + } else if (!(stats != null ? stats.isDirectory() : undefined)) { + logger.err( + { dir: compileDir, stats }, + 'bad project directory for removal' + ) + return callback(new Error('project directory is not directory')) + } else { + return callback(null, true) + } + }) + }, // directory exists + + syncFromCode(project_id, user_id, file_name, line, column, callback) { + // If LaTeX was run in a virtual environment, the file path that synctex expects + // might not match the file path on the host. The .synctex.gz file however, will be accessed + // wherever it is on the host. + if (callback == null) { + callback = function(error, pdfPositions) {} + } + const compileName = getCompileName(project_id, user_id) + const base_dir = Settings.path.synctexBaseDir(compileName) + const file_path = base_dir + '/' + file_name + const compileDir = getCompileDir(project_id, user_id) + const synctex_path = `${base_dir}/output.pdf` + const command = ['code', synctex_path, file_path, line, column] + return fse.ensureDir(compileDir, function(error) { + if (error != null) { + logger.err( + { error, project_id, user_id, file_name }, + 'error ensuring dir for sync from code' + ) + return callback(error) + } + return CompileManager._runSynctex(project_id, user_id, command, function( + error, + stdout + ) { + if (error != null) { + return callback(error) + } + logger.log( + { project_id, user_id, file_name, line, column, command, stdout }, + 'synctex code output' + ) + return callback( + null, + CompileManager._parseSynctexFromCodeOutput(stdout) + ) + }) + }) + }, + + syncFromPdf(project_id, user_id, page, h, v, callback) { + if (callback == null) { + callback = function(error, filePositions) {} + } + const compileName = getCompileName(project_id, user_id) + const compileDir = getCompileDir(project_id, user_id) + const base_dir = Settings.path.synctexBaseDir(compileName) + const synctex_path = `${base_dir}/output.pdf` + const command = ['pdf', synctex_path, page, h, v] + return fse.ensureDir(compileDir, function(error) { + if (error != null) { + logger.err( + { error, project_id, user_id, file_name }, + 'error ensuring dir for sync to code' + ) + return callback(error) + } + return CompileManager._runSynctex(project_id, user_id, command, function( + error, + stdout + ) { + if (error != null) { + return callback(error) + } + logger.log( + { project_id, user_id, page, h, v, stdout }, + 'synctex pdf output' + ) + return callback( + null, + CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) + ) + }) + }) + }, + + _checkFileExists(path, callback) { + if (callback == null) { + callback = function(error) {} + } + const synctexDir = Path.dirname(path) + const synctexFile = Path.join(synctexDir, 'output.synctex.gz') + return fs.stat(synctexDir, function(error, stats) { + if ((error != null ? error.code : undefined) === 'ENOENT') { + return callback( + new Errors.NotFoundError('called synctex with no output directory') + ) + } + if (error != null) { + return callback(error) + } + return fs.stat(synctexFile, function(error, stats) { + if ((error != null ? error.code : undefined) === 'ENOENT') { + return callback( + new Errors.NotFoundError('called synctex with no output file') + ) + } + if (error != null) { + return callback(error) + } + if (!(stats != null ? stats.isFile() : undefined)) { + return callback(new Error('not a file')) + } + return callback() + }) + }) + }, + + _runSynctex(project_id, user_id, command, callback) { + if (callback == null) { + callback = function(error, stdout) {} + } + const seconds = 1000 + + command.unshift('/opt/synctex') + + const directory = getCompileDir(project_id, user_id) + const timeout = 60 * 1000 // increased to allow for large projects + const compileName = getCompileName(project_id, user_id) + return CommandRunner.run( + compileName, + command, + directory, + Settings.clsi != null ? Settings.clsi.docker.image : undefined, + timeout, + {}, + function(error, output) { + if (error != null) { + logger.err( + { err: error, command, project_id, user_id }, + 'error running synctex' + ) + return callback(error) + } + return callback(null, output.stdout) + } + ) + }, + + _parseSynctexFromCodeOutput(output) { + const results = [] + for (const line of Array.from(output.split('\n'))) { + const [node, page, h, v, width, height] = Array.from(line.split('\t')) + if (node === 'NODE') { + results.push({ + page: parseInt(page, 10), + h: parseFloat(h), + v: parseFloat(v), + height: parseFloat(height), + width: parseFloat(width) + }) + } + } + return results + }, + + _parseSynctexFromPdfOutput(output, base_dir) { + const results = [] + for (let line of Array.from(output.split('\n'))) { + let column, file_path, node + ;[node, file_path, line, column] = Array.from(line.split('\t')) + if (node === 'NODE') { + const file = file_path.slice(base_dir.length + 1) + results.push({ + file, + line: parseInt(line, 10), + column: parseInt(column, 10) + }) + } + } + return results + }, + + wordcount(project_id, user_id, file_name, image, callback) { + if (callback == null) { + callback = function(error, pdfPositions) {} + } + logger.log({ project_id, user_id, file_name, image }, 'running wordcount') + const file_path = `$COMPILE_DIR/${file_name}` + const command = [ + 'texcount', + '-nocol', + '-inc', + file_path, + `-out=${file_path}.wc` + ] + const compileDir = getCompileDir(project_id, user_id) + const timeout = 60 * 1000 + const compileName = getCompileName(project_id, user_id) + return fse.ensureDir(compileDir, function(error) { + if (error != null) { + logger.err( + { error, project_id, user_id, file_name }, + 'error ensuring dir for sync from code' + ) + return callback(error) + } + return CommandRunner.run( + compileName, + command, + compileDir, + image, + timeout, + {}, + function(error) { + if (error != null) { + return callback(error) + } + return fs.readFile( + compileDir + '/' + file_name + '.wc', + 'utf-8', + function(err, stdout) { + if (err != null) { + // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored + logger.err( + { node_err: err, command, compileDir, project_id, user_id }, + 'error reading word count output' + ) + return callback(err) + } + const results = CompileManager._parseWordcountFromOutput(stdout) + logger.log( + { project_id, user_id, wordcount: results }, + 'word count results' + ) + return callback(null, results) + } + ) + } + ) + }) + }, + + _parseWordcountFromOutput(output) { + const results = { + encode: '', + textWords: 0, + headWords: 0, + outside: 0, + headers: 0, + elements: 0, + mathInline: 0, + mathDisplay: 0, + errors: 0, + messages: '' + } + for (const line of Array.from(output.split('\n'))) { + const [data, info] = Array.from(line.split(':')) + if (data.indexOf('Encoding') > -1) { + results.encode = info.trim() + } + if (data.indexOf('in text') > -1) { + results.textWords = parseInt(info, 10) + } + if (data.indexOf('in head') > -1) { + results.headWords = parseInt(info, 10) + } + if (data.indexOf('outside') > -1) { + results.outside = parseInt(info, 10) + } + if (data.indexOf('of head') > -1) { + results.headers = parseInt(info, 10) + } + if (data.indexOf('Number of floats/tables/figures') > -1) { + results.elements = parseInt(info, 10) + } + if (data.indexOf('Number of math inlines') > -1) { + results.mathInline = parseInt(info, 10) + } + if (data.indexOf('Number of math displayed') > -1) { + results.mathDisplay = parseInt(info, 10) + } + if (data === '(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 + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/app/js/ContentTypeMapper.js b/app/js/ContentTypeMapper.js index fdd66d38..f690bf9d 100644 --- a/app/js/ContentTypeMapper.js +++ b/app/js/ContentTypeMapper.js @@ -3,31 +3,36 @@ */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. -let ContentTypeMapper; -const Path = require('path'); +let ContentTypeMapper +const Path = require('path') // here we coerce html, css and js to text/plain, // otherwise choose correct mime type based on file extension, // falling back to octet-stream -module.exports = (ContentTypeMapper = { - map(path) { - switch (Path.extname(path)) { - case '.txt': case '.html': case '.js': case '.css': case '.svg': - return 'text/plain'; - case '.csv': - return 'text/csv'; - case '.pdf': - return 'application/pdf'; - case '.png': - return 'image/png'; - case '.jpg': case '.jpeg': - return 'image/jpeg'; - case '.tiff': - return 'image/tiff'; - case '.gif': - return 'image/gif'; - default: - return 'application/octet-stream'; - } - } -}); +module.exports = ContentTypeMapper = { + map(path) { + switch (Path.extname(path)) { + case '.txt': + case '.html': + case '.js': + case '.css': + case '.svg': + return 'text/plain' + case '.csv': + return 'text/csv' + case '.pdf': + return 'application/pdf' + case '.png': + return 'image/png' + case '.jpg': + case '.jpeg': + return 'image/jpeg' + case '.tiff': + return 'image/tiff' + case '.gif': + return 'image/gif' + default: + return 'application/octet-stream' + } + } +} diff --git a/app/js/DbQueue.js b/app/js/DbQueue.js index 89ff3234..7589370c 100644 --- a/app/js/DbQueue.js +++ b/app/js/DbQueue.js @@ -5,14 +5,14 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const async = require("async"); -const Settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); -const queue = async.queue((task, cb)=> task(cb) - , Settings.parallelSqlQueryLimit); +const async = require('async') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const queue = async.queue( + (task, cb) => task(cb), + Settings.parallelSqlQueryLimit +) -queue.drain = ()=> logger.debug('all items have been processed'); - -module.exports = - {queue}; +queue.drain = () => logger.debug('all items have been processed') +module.exports = { queue } diff --git a/app/js/DockerLockManager.js b/app/js/DockerLockManager.js index 274ff66c..2685b42d 100644 --- a/app/js/DockerLockManager.js +++ b/app/js/DockerLockManager.js @@ -10,80 +10,104 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let LockManager; -const logger = require("logger-sharelatex"); +let LockManager +const logger = require('logger-sharelatex') -const LockState = {}; // locks for docker container operations, by container name +const LockState = {} // locks for docker container operations, by container name -module.exports = (LockManager = { +module.exports = LockManager = { + MAX_LOCK_HOLD_TIME: 15000, // how long we can keep a lock + MAX_LOCK_WAIT_TIME: 10000, // how long we wait for a lock + LOCK_TEST_INTERVAL: 1000, // retry time - MAX_LOCK_HOLD_TIME: 15000, // how long we can keep a lock - MAX_LOCK_WAIT_TIME: 10000, // how long we wait for a lock - LOCK_TEST_INTERVAL: 1000, // retry time + tryLock(key, callback) { + let lockValue + if (callback == null) { + callback = function(err, gotLock) {} + } + const existingLock = LockState[key] + if (existingLock != null) { + // the lock is already taken, check how old it is + const lockAge = Date.now() - existingLock.created + if (lockAge < LockManager.MAX_LOCK_HOLD_TIME) { + return callback(null, false) // we didn't get the lock, bail out + } else { + logger.error( + { key, lock: existingLock, age: lockAge }, + 'taking old lock by force' + ) + } + } + // take the lock + LockState[key] = lockValue = { created: Date.now() } + return callback(null, true, lockValue) + }, - tryLock(key, callback) { - let lockValue; - if (callback == null) { callback = function(err, gotLock) {}; } - const existingLock = LockState[key]; - if (existingLock != null) { // the lock is already taken, check how old it is - const lockAge = Date.now() - existingLock.created; - if (lockAge < LockManager.MAX_LOCK_HOLD_TIME) { - return callback(null, false); // we didn't get the lock, bail out - } else { - logger.error({key, lock: existingLock, age:lockAge}, "taking old lock by force"); - } - } - // take the lock - LockState[key] = (lockValue = {created: Date.now()}); - return callback(null, true, lockValue); - }, + getLock(key, callback) { + let attempt + if (callback == null) { + callback = function(error, lockValue) {} + } + const startTime = Date.now() + return (attempt = () => + LockManager.tryLock(key, function(error, gotLock, lockValue) { + if (error != null) { + return callback(error) + } + if (gotLock) { + return callback(null, lockValue) + } else if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) { + const e = new Error('Lock timeout') + e.key = key + return callback(e) + } else { + return setTimeout(attempt, LockManager.LOCK_TEST_INTERVAL) + } + }))() + }, - getLock(key, callback) { - let attempt; - if (callback == null) { callback = function(error, lockValue) {}; } - const startTime = Date.now(); - return (attempt = () => - LockManager.tryLock(key, function(error, gotLock, lockValue) { - if (error != null) { return callback(error); } - if (gotLock) { - return callback(null, lockValue); - } else if ((Date.now() - startTime) > LockManager.MAX_LOCK_WAIT_TIME) { - const e = new Error("Lock timeout"); - e.key = key; - return callback(e); - } else { - return setTimeout(attempt, LockManager.LOCK_TEST_INTERVAL); - } - }) - )(); - }, + releaseLock(key, lockValue, callback) { + if (callback == null) { + callback = function(error) {} + } + const existingLock = LockState[key] + if (existingLock === lockValue) { + // lockValue is an object, so we can test by reference + delete LockState[key] // our lock, so we can free it + return callback() + } else if (existingLock != null) { + // lock exists but doesn't match ours + logger.error( + { key, lock: existingLock }, + 'tried to release lock taken by force' + ) + return callback() + } else { + logger.error( + { key, lock: existingLock }, + 'tried to release lock that has gone' + ) + return callback() + } + }, - releaseLock(key, lockValue, callback) { - if (callback == null) { callback = function(error) {}; } - const existingLock = LockState[key]; - if (existingLock === lockValue) { // lockValue is an object, so we can test by reference - delete LockState[key]; // our lock, so we can free it - return callback(); - } else if (existingLock != null) { // lock exists but doesn't match ours - logger.error({key, lock: existingLock}, "tried to release lock taken by force"); - return callback(); - } else { - logger.error({key, lock: existingLock}, "tried to release lock that has gone"); - return callback(); - } - }, - - runWithLock(key, runner, callback) { - if (callback == null) { callback = function(error) {}; } - return LockManager.getLock(key, function(error, lockValue) { - if (error != null) { return callback(error); } - return runner((error1, ...args) => - LockManager.releaseLock(key, lockValue, function(error2) { - error = error1 || error2; - if (error != null) { return callback(error); } - return callback(null, ...Array.from(args)); - }) - ); - }); - } -}); + runWithLock(key, runner, callback) { + if (callback == null) { + callback = function(error) {} + } + return LockManager.getLock(key, function(error, lockValue) { + if (error != null) { + return callback(error) + } + return runner((error1, ...args) => + LockManager.releaseLock(key, lockValue, function(error2) { + error = error1 || error2 + if (error != null) { + return callback(error) + } + return callback(null, ...Array.from(args)) + }) + ) + }) + } +} diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index dc04b5df..5ac234b7 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -15,469 +15,666 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let DockerRunner, oneHour; -const Settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); -const Docker = require("dockerode"); -const dockerode = new Docker(); -const crypto = require("crypto"); -const async = require("async"); -const LockManager = require("./DockerLockManager"); -const fs = require("fs"); -const Path = require('path'); -const _ = require("underscore"); - -logger.info("using docker runner"); - -const usingSiblingContainers = () => __guard__(Settings != null ? Settings.path : undefined, x => x.sandboxedCompilesHostDir) != null; - -module.exports = (DockerRunner = { - ERR_NOT_DIRECTORY: new Error("not a directory"), - ERR_TERMINATED: new Error("terminated"), - ERR_EXITED: new Error("exited"), - ERR_TIMED_OUT: new Error("container timed out"), - - run(project_id, command, directory, image, timeout, environment, callback) { - - let name; - if (callback == null) { callback = function(error, output) {}; } - if (usingSiblingContainers()) { - const _newPath = Settings.path.sandboxedCompilesHostDir; - logger.log({path: _newPath}, "altering bind path for sibling containers"); - // Server Pro, example: - // '/var/lib/sharelatex/data/compiles/<project-id>' - // ... becomes ... - // '/opt/sharelatex_data/data/compiles/<project-id>' - directory = Path.join(Settings.path.sandboxedCompilesHostDir, Path.basename(directory)); - } - - const volumes = {}; - volumes[directory] = "/compile"; - - command = (Array.from(command).map((arg) => __guardMethod__(arg.toString(), 'replace', o => o.replace('$COMPILE_DIR', "/compile")))); - if ((image == null)) { - ({ image } = Settings.clsi.docker); - } - - if (Settings.texliveImageNameOveride != null) { - const img = image.split("/"); - image = `${Settings.texliveImageNameOveride}/${img[2]}`; - } - - const options = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment); - const fingerprint = DockerRunner._fingerprintContainer(options); - options.name = (name = `project-${project_id}-${fingerprint}`); - - // logOptions = _.clone(options) - // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" - logger.log({project_id}, "running docker container"); - DockerRunner._runAndWaitForContainer(options, volumes, timeout, function(error, output) { - if (__guard__(error != null ? error.message : undefined, x => x.match("HTTP code is 500"))) { - logger.log({err: error, project_id}, "error running container so destroying and retrying"); - return DockerRunner.destroyContainer(name, null, true, function(error) { - if (error != null) { return callback(error); } - return DockerRunner._runAndWaitForContainer(options, volumes, timeout, callback); - }); - } else { - return callback(error, output); - } - }); - - return name; - }, // pass back the container name to allow it to be killed - - kill(container_id, callback) { - if (callback == null) { callback = function(error) {}; } - logger.log({container_id}, "sending kill signal to container"); - const container = dockerode.getContainer(container_id); - return container.kill(function(error) { - if ((error != null) && __guardMethod__(error != null ? error.message : undefined, 'match', o => o.match(/Cannot kill container .* is not running/))) { - logger.warn({err: error, container_id}, "container not running, continuing"); - error = null; - } - if (error != null) { - logger.error({err: error, container_id}, "error killing container"); - return callback(error); - } else { - return callback(); - } - }); - }, - - _runAndWaitForContainer(options, volumes, timeout, _callback) { - if (_callback == null) { _callback = function(error, output) {}; } - const callback = function(...args) { - _callback(...Array.from(args || [])); - // Only call the callback once - return _callback = function() {}; - }; - - const { name } = options; - - let streamEnded = false; - let containerReturned = false; - let output = {}; - - const callbackIfFinished = function() { - if (streamEnded && containerReturned) { - return callback(null, output); - } - }; - - const attachStreamHandler = function(error, _output) { - if (error != null) { return callback(error); } - output = _output; - streamEnded = true; - return callbackIfFinished(); - }; - - return DockerRunner.startContainer(options, volumes, attachStreamHandler, function(error, containerId) { - if (error != null) { return callback(error); } - - return DockerRunner.waitForContainer(name, timeout, function(error, exitCode) { - let err; - if (error != null) { return callback(error); } - if (exitCode === 137) { // exit status from kill -9 - err = DockerRunner.ERR_TERMINATED; - err.terminated = true; - return callback(err); - } - if (exitCode === 1) { // exit status from chktex - err = DockerRunner.ERR_EXITED; - err.code = exitCode; - return callback(err); - } - containerReturned = true; - __guard__(options != null ? options.HostConfig : undefined, x => x.SecurityOpt = null); // small log line - logger.log({err, exitCode, options}, "docker container has exited"); - return callbackIfFinished(); - }); - }); - }, - - _getContainerOptions(command, image, volumes, timeout, environment) { - let m, year; - let key, value, hostVol, dockerVol; - const timeoutInSeconds = timeout / 1000; - - const dockerVolumes = {}; - for (hostVol in volumes) { - dockerVol = volumes[hostVol]; - dockerVolumes[dockerVol] = {}; - - if (volumes[hostVol].slice(-3).indexOf(":r") === -1) { - volumes[hostVol] = `${dockerVol}:rw`; - } - } - - // merge settings and environment parameter - const env = {}; - for (const src of [Settings.clsi.docker.env, environment || {}]) { - for (key in src) { value = src[key]; env[key] = value; } - } - // set the path based on the image year - if ((m = image.match(/:([0-9]+)\.[0-9]+/))) { - year = m[1]; - } else { - year = "2014"; - } - env.PATH = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/`; - const options = { - "Cmd" : command, - "Image" : image, - "Volumes" : dockerVolumes, - "WorkingDir" : "/compile", - "NetworkDisabled" : true, - "Memory" : 1024 * 1024 * 1024 * 1024, // 1 Gb - "User" : Settings.clsi.docker.user, - "Env" : (((() => { - const result = []; - for (key in env) { - value = env[key]; - result.push(`${key}=${value}`); - } - return result; - })())), // convert the environment hash to an array - "HostConfig" : { - "Binds": (((() => { - const result1 = []; - for (hostVol in volumes) { - dockerVol = volumes[hostVol]; - result1.push(`${hostVol}:${dockerVol}`); - } - return result1; - })())), - "LogConfig": {"Type": "none", "Config": {}}, - "Ulimits": [{'Name': 'cpu', 'Soft': timeoutInSeconds+5, 'Hard': timeoutInSeconds+10}], - "CapDrop": "ALL", - "SecurityOpt": ["no-new-privileges"] - } - }; - - - if ((Settings.path != null ? Settings.path.synctexBinHostPath : undefined) != null) { - options.HostConfig.Binds.push(`${Settings.path.synctexBinHostPath}:/opt/synctex:ro`); - } - - if (Settings.clsi.docker.seccomp_profile != null) { - options.HostConfig.SecurityOpt.push(`seccomp=${Settings.clsi.docker.seccomp_profile}`); - } - - return options; - }, - - _fingerprintContainer(containerOptions) { - // Yay, Hashing! - const json = JSON.stringify(containerOptions); - return crypto.createHash("md5").update(json).digest("hex"); - }, - - startContainer(options, volumes, attachStreamHandler, callback) { - return LockManager.runWithLock(options.name, releaseLock => - // Check that volumes exist before starting the container. - // When a container is started with volume pointing to a - // non-existent directory then docker creates the directory but - // with root ownership. - DockerRunner._checkVolumes(options, volumes, function(err) { - if (err != null) { return releaseLock(err); } - return DockerRunner._startContainer(options, volumes, attachStreamHandler, releaseLock); - }) - - , callback); - }, - - // Check that volumes exist and are directories - _checkVolumes(options, volumes, callback) { - if (callback == null) { callback = function(error, containerName) {}; } - if (usingSiblingContainers()) { - // Server Pro, with sibling-containers active, skip checks - return callback(null); - } - - const checkVolume = (path, cb) => - fs.stat(path, function(err, stats) { - if (err != null) { return cb(err); } - if (!(stats != null ? stats.isDirectory() : undefined)) { return cb(DockerRunner.ERR_NOT_DIRECTORY); } - return cb(); - }) - ; - const jobs = []; - for (const vol in volumes) { - (vol => jobs.push(cb => checkVolume(vol, cb)))(vol); - } - return async.series(jobs, callback); - }, - - _startContainer(options, volumes, attachStreamHandler, callback) { - if (callback == null) { callback = function(error, output) {}; } - callback = _.once(callback); - const { name } = options; - - logger.log({container_name: name}, "starting container"); - const container = dockerode.getContainer(name); - - const createAndStartContainer = () => - dockerode.createContainer(options, function(error, container) { - if (error != null) { return callback(error); } - return startExistingContainer(); - }) - ; - - var startExistingContainer = () => - DockerRunner.attachToContainer(options.name, attachStreamHandler, function(error){ - if (error != null) { return callback(error); } - return container.start(function(error) { - if ((error != null) && ((error != null ? error.statusCode : undefined) !== 304)) { // already running - return callback(error); - } else { - return callback(); - } - }); - }) - ; - - return container.inspect(function(error, stats){ - if ((error != null ? error.statusCode : undefined) === 404) { - return createAndStartContainer(); - } else if (error != null) { - logger.err({container_name: name, error}, "unable to inspect container to start"); - return callback(error); - } else { - return startExistingContainer(); - } - }); - }, - - - attachToContainer(containerId, attachStreamHandler, attachStartCallback) { - const container = dockerode.getContainer(containerId); - return container.attach({stdout: 1, stderr: 1, stream: 1}, function(error, stream) { - if (error != null) { - logger.error({err: error, container_id: containerId}, "error attaching to container"); - return attachStartCallback(error); - } else { - attachStartCallback(); - } - - - logger.log({container_id: containerId}, "attached to container"); - - const MAX_OUTPUT = 1024 * 1024; // limit output to 1MB - const createStringOutputStream = function(name) { - return { - data: "", - overflowed: false, - write(data) { - if (this.overflowed) { return; } - if (this.data.length < MAX_OUTPUT) { - return this.data += data; - } else { - logger.error({container_id: containerId, length: this.data.length, maxLen: MAX_OUTPUT}, `${name} exceeds max size`); - this.data += `(...truncated at ${MAX_OUTPUT} chars...)`; - return this.overflowed = true; - } - } - // kill container if too much output - // docker.containers.kill(containerId, () ->) - }; - }; - - const stdout = createStringOutputStream("stdout"); - const stderr = createStringOutputStream("stderr"); - - container.modem.demuxStream(stream, stdout, stderr); - - stream.on("error", err => logger.error({err, container_id: containerId}, "error reading from container stream")); - - return stream.on("end", () => attachStreamHandler(null, {stdout: stdout.data, stderr: stderr.data})); - }); - }, - - waitForContainer(containerId, timeout, _callback) { - if (_callback == null) { _callback = function(error, exitCode) {}; } - const callback = function(...args) { - _callback(...Array.from(args || [])); - // Only call the callback once - return _callback = function() {}; - }; - - const container = dockerode.getContainer(containerId); - - let timedOut = false; - const timeoutId = setTimeout(function() { - timedOut = true; - logger.log({container_id: containerId}, "timeout reached, killing container"); - return container.kill(function() {}); - } - , timeout); - - logger.log({container_id: containerId}, "waiting for docker container"); - return container.wait(function(error, res) { - if (error != null) { - clearTimeout(timeoutId); - logger.error({err: error, container_id: containerId}, "error waiting for container"); - return callback(error); - } - if (timedOut) { - logger.log({containerId}, "docker container timed out"); - error = DockerRunner.ERR_TIMED_OUT; - error.timedout = true; - return callback(error); - } else { - clearTimeout(timeoutId); - logger.log({container_id: containerId, exitCode: res.StatusCode}, "docker container returned"); - return callback(null, res.StatusCode); - } - }); - }, - - destroyContainer(containerName, containerId, shouldForce, callback) { - // We want the containerName for the lock and, ideally, the - // containerId to delete. There is a bug in the docker.io module - // where if you delete by name and there is an error, it throws an - // async exception, but if you delete by id it just does a normal - // error callback. We fall back to deleting by name if no id is - // supplied. - if (callback == null) { callback = function(error) {}; } - return LockManager.runWithLock(containerName, releaseLock => DockerRunner._destroyContainer(containerId || containerName, shouldForce, releaseLock) - , callback); - }, - - _destroyContainer(containerId, shouldForce, callback) { - if (callback == null) { callback = function(error) {}; } - logger.log({container_id: containerId}, "destroying docker container"); - const container = dockerode.getContainer(containerId); - return container.remove({force: shouldForce === true}, function(error) { - if ((error != null) && ((error != null ? error.statusCode : undefined) === 404)) { - logger.warn({err: error, container_id: containerId}, "container not found, continuing"); - error = null; - } - if (error != null) { - logger.error({err: error, container_id: containerId}, "error destroying container"); - } else { - logger.log({container_id: containerId}, "destroyed container"); - } - return callback(error); - }); - }, - - // handle expiry of docker containers - - MAX_CONTAINER_AGE: Settings.clsi.docker.maxContainerAge || (oneHour = 60 * 60 * 1000), - - examineOldContainer(container, callback) { - if (callback == null) { callback = function(error, name, id, ttl){}; } - const name = container.Name || (container.Names != null ? container.Names[0] : undefined); - const created = container.Created * 1000; // creation time is returned in seconds - const now = Date.now(); - const age = now - created; - const maxAge = DockerRunner.MAX_CONTAINER_AGE; - const ttl = maxAge - age; - logger.log({containerName: name, created, now, age, maxAge, ttl}, "checking whether to destroy container"); - return callback(null, name, container.Id, ttl); - }, - - destroyOldContainers(callback) { - if (callback == null) { callback = function(error) {}; } - return dockerode.listContainers({all: true}, function(error, containers) { - if (error != null) { return callback(error); } - const jobs = []; - for (const container of Array.from(containers || [])) { - (container => - DockerRunner.examineOldContainer(container, function(err, name, id, ttl) { - if ((name.slice(0, 9) === '/project-') && (ttl <= 0)) { - return jobs.push(cb => DockerRunner.destroyContainer(name, id, false, () => cb())); - } - }) - )(container); - } - // Ignore errors because some containers get stuck but - // will be destroyed next time - return async.series(jobs, callback); - }); - }, - - startContainerMonitor() { - logger.log({maxAge: DockerRunner.MAX_CONTAINER_AGE}, "starting container expiry"); - // randomise the start time - const randomDelay = Math.floor(Math.random() * 5 * 60 * 1000); - return setTimeout(() => - setInterval(() => DockerRunner.destroyOldContainers() - , (oneHour = 60 * 60 * 1000)) - - , randomDelay); - } -}); - -DockerRunner.startContainerMonitor(); +let DockerRunner, oneHour +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const Docker = require('dockerode') +const dockerode = new Docker() +const crypto = require('crypto') +const async = require('async') +const LockManager = require('./DockerLockManager') +const fs = require('fs') +const Path = require('path') +const _ = require('underscore') + +logger.info('using docker runner') + +const usingSiblingContainers = () => + __guard__( + Settings != null ? Settings.path : undefined, + x => x.sandboxedCompilesHostDir + ) != null + +module.exports = DockerRunner = { + ERR_NOT_DIRECTORY: new Error('not a directory'), + ERR_TERMINATED: new Error('terminated'), + ERR_EXITED: new Error('exited'), + ERR_TIMED_OUT: new Error('container timed out'), + + run(project_id, command, directory, image, timeout, environment, callback) { + let name + if (callback == null) { + callback = function(error, output) {} + } + if (usingSiblingContainers()) { + const _newPath = Settings.path.sandboxedCompilesHostDir + logger.log( + { path: _newPath }, + 'altering bind path for sibling containers' + ) + // Server Pro, example: + // '/var/lib/sharelatex/data/compiles/<project-id>' + // ... becomes ... + // '/opt/sharelatex_data/data/compiles/<project-id>' + directory = Path.join( + Settings.path.sandboxedCompilesHostDir, + Path.basename(directory) + ) + } + + const volumes = {} + volumes[directory] = '/compile' + + command = Array.from(command).map(arg => + __guardMethod__(arg.toString(), 'replace', o => + o.replace('$COMPILE_DIR', '/compile') + ) + ) + if (image == null) { + ;({ image } = Settings.clsi.docker) + } + + if (Settings.texliveImageNameOveride != null) { + const img = image.split('/') + image = `${Settings.texliveImageNameOveride}/${img[2]}` + } + + const options = DockerRunner._getContainerOptions( + command, + image, + volumes, + timeout, + environment + ) + const fingerprint = DockerRunner._fingerprintContainer(options) + options.name = name = `project-${project_id}-${fingerprint}` + + // logOptions = _.clone(options) + // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" + logger.log({ project_id }, 'running docker container') + DockerRunner._runAndWaitForContainer(options, volumes, timeout, function( + error, + output + ) { + if ( + __guard__(error != null ? error.message : undefined, x => + x.match('HTTP code is 500') + ) + ) { + logger.log( + { err: error, project_id }, + 'error running container so destroying and retrying' + ) + return DockerRunner.destroyContainer(name, null, true, function(error) { + if (error != null) { + return callback(error) + } + return DockerRunner._runAndWaitForContainer( + options, + volumes, + timeout, + callback + ) + }) + } else { + return callback(error, output) + } + }) + + return name + }, // pass back the container name to allow it to be killed + + kill(container_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ container_id }, 'sending kill signal to container') + const container = dockerode.getContainer(container_id) + return container.kill(function(error) { + if ( + error != null && + __guardMethod__(error != null ? error.message : undefined, 'match', o => + o.match(/Cannot kill container .* is not running/) + ) + ) { + logger.warn( + { err: error, container_id }, + 'container not running, continuing' + ) + error = null + } + if (error != null) { + logger.error({ err: error, container_id }, 'error killing container') + return callback(error) + } else { + return callback() + } + }) + }, + + _runAndWaitForContainer(options, volumes, timeout, _callback) { + if (_callback == null) { + _callback = function(error, output) {} + } + const callback = function(...args) { + _callback(...Array.from(args || [])) + // Only call the callback once + return (_callback = function() {}) + } + + const { name } = options + + let streamEnded = false + let containerReturned = false + let output = {} + + const callbackIfFinished = function() { + if (streamEnded && containerReturned) { + return callback(null, output) + } + } + + const attachStreamHandler = function(error, _output) { + if (error != null) { + return callback(error) + } + output = _output + streamEnded = true + return callbackIfFinished() + } + + return DockerRunner.startContainer( + options, + volumes, + attachStreamHandler, + function(error, containerId) { + if (error != null) { + return callback(error) + } + + return DockerRunner.waitForContainer(name, timeout, function( + error, + exitCode + ) { + let err + if (error != null) { + return callback(error) + } + if (exitCode === 137) { + // exit status from kill -9 + err = DockerRunner.ERR_TERMINATED + err.terminated = true + return callback(err) + } + if (exitCode === 1) { + // exit status from chktex + err = DockerRunner.ERR_EXITED + err.code = exitCode + return callback(err) + } + containerReturned = true + __guard__( + options != null ? options.HostConfig : undefined, + x => (x.SecurityOpt = null) + ) // small log line + logger.log({ err, exitCode, options }, 'docker container has exited') + return callbackIfFinished() + }) + } + ) + }, + + _getContainerOptions(command, image, volumes, timeout, environment) { + let m, year + let key, value, hostVol, dockerVol + const timeoutInSeconds = timeout / 1000 + + const dockerVolumes = {} + for (hostVol in volumes) { + dockerVol = volumes[hostVol] + dockerVolumes[dockerVol] = {} + + if (volumes[hostVol].slice(-3).indexOf(':r') === -1) { + volumes[hostVol] = `${dockerVol}:rw` + } + } + + // merge settings and environment parameter + const env = {} + for (const src of [Settings.clsi.docker.env, environment || {}]) { + for (key in src) { + value = src[key] + env[key] = value + } + } + // set the path based on the image year + if ((m = image.match(/:([0-9]+)\.[0-9]+/))) { + year = m[1] + } else { + year = '2014' + } + env.PATH = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/` + const options = { + Cmd: command, + Image: image, + Volumes: dockerVolumes, + WorkingDir: '/compile', + NetworkDisabled: true, + Memory: 1024 * 1024 * 1024 * 1024, // 1 Gb + User: Settings.clsi.docker.user, + Env: (() => { + const result = [] + for (key in env) { + value = env[key] + result.push(`${key}=${value}`) + } + return result + })(), // convert the environment hash to an array + HostConfig: { + Binds: (() => { + const result1 = [] + for (hostVol in volumes) { + dockerVol = volumes[hostVol] + result1.push(`${hostVol}:${dockerVol}`) + } + return result1 + })(), + LogConfig: { Type: 'none', Config: {} }, + Ulimits: [ + { + Name: 'cpu', + Soft: timeoutInSeconds + 5, + Hard: timeoutInSeconds + 10 + } + ], + CapDrop: 'ALL', + SecurityOpt: ['no-new-privileges'] + } + } + + if ( + (Settings.path != null ? Settings.path.synctexBinHostPath : undefined) != + null + ) { + options.HostConfig.Binds.push( + `${Settings.path.synctexBinHostPath}:/opt/synctex:ro` + ) + } + + if (Settings.clsi.docker.seccomp_profile != null) { + options.HostConfig.SecurityOpt.push( + `seccomp=${Settings.clsi.docker.seccomp_profile}` + ) + } + + return options + }, + + _fingerprintContainer(containerOptions) { + // Yay, Hashing! + const json = JSON.stringify(containerOptions) + return crypto + .createHash('md5') + .update(json) + .digest('hex') + }, + + startContainer(options, volumes, attachStreamHandler, callback) { + return LockManager.runWithLock( + options.name, + releaseLock => + // Check that volumes exist before starting the container. + // When a container is started with volume pointing to a + // non-existent directory then docker creates the directory but + // with root ownership. + DockerRunner._checkVolumes(options, volumes, function(err) { + if (err != null) { + return releaseLock(err) + } + return DockerRunner._startContainer( + options, + volumes, + attachStreamHandler, + releaseLock + ) + }), + + callback + ) + }, + + // Check that volumes exist and are directories + _checkVolumes(options, volumes, callback) { + if (callback == null) { + callback = function(error, containerName) {} + } + if (usingSiblingContainers()) { + // Server Pro, with sibling-containers active, skip checks + return callback(null) + } + + const checkVolume = (path, cb) => + fs.stat(path, function(err, stats) { + if (err != null) { + return cb(err) + } + if (!(stats != null ? stats.isDirectory() : undefined)) { + return cb(DockerRunner.ERR_NOT_DIRECTORY) + } + return cb() + }) + const jobs = [] + for (const vol in volumes) { + ;(vol => jobs.push(cb => checkVolume(vol, cb)))(vol) + } + return async.series(jobs, callback) + }, + + _startContainer(options, volumes, attachStreamHandler, callback) { + if (callback == null) { + callback = function(error, output) {} + } + callback = _.once(callback) + const { name } = options + + logger.log({ container_name: name }, 'starting container') + const container = dockerode.getContainer(name) + + const createAndStartContainer = () => + dockerode.createContainer(options, function(error, container) { + if (error != null) { + return callback(error) + } + return startExistingContainer() + }) + var startExistingContainer = () => + DockerRunner.attachToContainer( + options.name, + attachStreamHandler, + function(error) { + if (error != null) { + return callback(error) + } + return container.start(function(error) { + if ( + error != null && + (error != null ? error.statusCode : undefined) !== 304 + ) { + // already running + return callback(error) + } else { + return callback() + } + }) + } + ) + return container.inspect(function(error, stats) { + if ((error != null ? error.statusCode : undefined) === 404) { + return createAndStartContainer() + } else if (error != null) { + logger.err( + { container_name: name, error }, + 'unable to inspect container to start' + ) + return callback(error) + } else { + return startExistingContainer() + } + }) + }, + + attachToContainer(containerId, attachStreamHandler, attachStartCallback) { + const container = dockerode.getContainer(containerId) + return container.attach({ stdout: 1, stderr: 1, stream: 1 }, function( + error, + stream + ) { + if (error != null) { + logger.error( + { err: error, container_id: containerId }, + 'error attaching to container' + ) + return attachStartCallback(error) + } else { + attachStartCallback() + } + + logger.log({ container_id: containerId }, 'attached to container') + + const MAX_OUTPUT = 1024 * 1024 // limit output to 1MB + const createStringOutputStream = function(name) { + return { + data: '', + overflowed: false, + write(data) { + if (this.overflowed) { + return + } + if (this.data.length < MAX_OUTPUT) { + return (this.data += data) + } else { + logger.error( + { + container_id: containerId, + length: this.data.length, + maxLen: MAX_OUTPUT + }, + `${name} exceeds max size` + ) + this.data += `(...truncated at ${MAX_OUTPUT} chars...)` + return (this.overflowed = true) + } + } + // kill container if too much output + // docker.containers.kill(containerId, () ->) + } + } + + const stdout = createStringOutputStream('stdout') + const stderr = createStringOutputStream('stderr') + + container.modem.demuxStream(stream, stdout, stderr) + + stream.on('error', err => + logger.error( + { err, container_id: containerId }, + 'error reading from container stream' + ) + ) + + return stream.on('end', () => + attachStreamHandler(null, { stdout: stdout.data, stderr: stderr.data }) + ) + }) + }, + + waitForContainer(containerId, timeout, _callback) { + if (_callback == null) { + _callback = function(error, exitCode) {} + } + const callback = function(...args) { + _callback(...Array.from(args || [])) + // Only call the callback once + return (_callback = function() {}) + } + + const container = dockerode.getContainer(containerId) + + let timedOut = false + const timeoutId = setTimeout(function() { + timedOut = true + logger.log( + { container_id: containerId }, + 'timeout reached, killing container' + ) + return container.kill(function() {}) + }, timeout) + + logger.log({ container_id: containerId }, 'waiting for docker container') + return container.wait(function(error, res) { + if (error != null) { + clearTimeout(timeoutId) + logger.error( + { err: error, container_id: containerId }, + 'error waiting for container' + ) + return callback(error) + } + if (timedOut) { + logger.log({ containerId }, 'docker container timed out') + error = DockerRunner.ERR_TIMED_OUT + error.timedout = true + return callback(error) + } else { + clearTimeout(timeoutId) + logger.log( + { container_id: containerId, exitCode: res.StatusCode }, + 'docker container returned' + ) + return callback(null, res.StatusCode) + } + }) + }, + + destroyContainer(containerName, containerId, shouldForce, callback) { + // We want the containerName for the lock and, ideally, the + // containerId to delete. There is a bug in the docker.io module + // where if you delete by name and there is an error, it throws an + // async exception, but if you delete by id it just does a normal + // error callback. We fall back to deleting by name if no id is + // supplied. + if (callback == null) { + callback = function(error) {} + } + return LockManager.runWithLock( + containerName, + releaseLock => + DockerRunner._destroyContainer( + containerId || containerName, + shouldForce, + releaseLock + ), + callback + ) + }, + + _destroyContainer(containerId, shouldForce, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ container_id: containerId }, 'destroying docker container') + const container = dockerode.getContainer(containerId) + return container.remove({ force: shouldForce === true }, function(error) { + if ( + error != null && + (error != null ? error.statusCode : undefined) === 404 + ) { + logger.warn( + { err: error, container_id: containerId }, + 'container not found, continuing' + ) + error = null + } + if (error != null) { + logger.error( + { err: error, container_id: containerId }, + 'error destroying container' + ) + } else { + logger.log({ container_id: containerId }, 'destroyed container') + } + return callback(error) + }) + }, + + // handle expiry of docker containers + + MAX_CONTAINER_AGE: + Settings.clsi.docker.maxContainerAge || (oneHour = 60 * 60 * 1000), + + examineOldContainer(container, callback) { + if (callback == null) { + callback = function(error, name, id, ttl) {} + } + const name = + container.Name || + (container.Names != null ? container.Names[0] : undefined) + const created = container.Created * 1000 // creation time is returned in seconds + const now = Date.now() + const age = now - created + const maxAge = DockerRunner.MAX_CONTAINER_AGE + const ttl = maxAge - age + logger.log( + { containerName: name, created, now, age, maxAge, ttl }, + 'checking whether to destroy container' + ) + return callback(null, name, container.Id, ttl) + }, + + destroyOldContainers(callback) { + if (callback == null) { + callback = function(error) {} + } + return dockerode.listContainers({ all: true }, function(error, containers) { + if (error != null) { + return callback(error) + } + const jobs = [] + for (const container of Array.from(containers || [])) { + ;(container => + DockerRunner.examineOldContainer(container, function( + err, + name, + id, + ttl + ) { + if (name.slice(0, 9) === '/project-' && ttl <= 0) { + return jobs.push(cb => + DockerRunner.destroyContainer(name, id, false, () => cb()) + ) + } + }))(container) + } + // Ignore errors because some containers get stuck but + // will be destroyed next time + return async.series(jobs, callback) + }) + }, + + startContainerMonitor() { + logger.log( + { maxAge: DockerRunner.MAX_CONTAINER_AGE }, + 'starting container expiry' + ) + // randomise the start time + const randomDelay = Math.floor(Math.random() * 5 * 60 * 1000) + return setTimeout( + () => + setInterval( + () => DockerRunner.destroyOldContainers(), + (oneHour = 60 * 60 * 1000) + ), + + randomDelay + ) + } +} + +DockerRunner.startContainerMonitor() function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined } function __guardMethod__(obj, methodName, transform) { - if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { - return transform(obj, methodName); + if ( + typeof obj !== 'undefined' && + obj !== null && + typeof obj[methodName] === 'function' + ) { + return transform(obj, methodName) } else { - return undefined; + return undefined } -} \ No newline at end of file +} diff --git a/app/js/DraftModeManager.js b/app/js/DraftModeManager.js index 79f39ab2..c8f59aa6 100644 --- a/app/js/DraftModeManager.js +++ b/app/js/DraftModeManager.js @@ -11,34 +11,47 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let DraftModeManager; -const fs = require("fs"); -const logger = require("logger-sharelatex"); +let DraftModeManager +const fs = require('fs') +const logger = require('logger-sharelatex') -module.exports = (DraftModeManager = { - injectDraftMode(filename, callback) { - if (callback == null) { callback = function(error) {}; } - return fs.readFile(filename, "utf8", function(error, content) { - if (error != null) { return callback(error); } - // avoid adding draft mode more than once - if ((content != null ? content.indexOf("\\documentclass\[draft") : undefined) >= 0) { - return callback(); - } - const modified_content = DraftModeManager._injectDraftOption(content); - logger.log({ - content: content.slice(0,1024), // \documentclass is normally v near the top - modified_content: modified_content.slice(0,1024), - filename - }, "injected draft class"); - return fs.writeFile(filename, modified_content, callback); - }); - }, - - _injectDraftOption(content) { - return content - // With existing options (must be first, otherwise both are applied) - .replace(/\\documentclass\[/g, "\\documentclass[draft,") - // Without existing options - .replace(/\\documentclass\{/g, "\\documentclass[draft]{"); - } -}); +module.exports = DraftModeManager = { + injectDraftMode(filename, callback) { + if (callback == null) { + callback = function(error) {} + } + return fs.readFile(filename, 'utf8', function(error, content) { + if (error != null) { + return callback(error) + } + // avoid adding draft mode more than once + if ( + (content != null + ? content.indexOf('\\documentclass[draft') + : undefined) >= 0 + ) { + return callback() + } + const modified_content = DraftModeManager._injectDraftOption(content) + logger.log( + { + content: content.slice(0, 1024), // \documentclass is normally v near the top + modified_content: modified_content.slice(0, 1024), + filename + }, + 'injected draft class' + ) + return fs.writeFile(filename, modified_content, callback) + }) + }, + + _injectDraftOption(content) { + return ( + content + // With existing options (must be first, otherwise both are applied) + .replace(/\\documentclass\[/g, '\\documentclass[draft,') + // Without existing options + .replace(/\\documentclass\{/g, '\\documentclass[draft]{') + ) + } +} diff --git a/app/js/Errors.js b/app/js/Errors.js index e7ace2c2..d3a5f5a0 100644 --- a/app/js/Errors.js +++ b/app/js/Errors.js @@ -4,33 +4,33 @@ */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. -let Errors; +let Errors var NotFoundError = function(message) { - const error = new Error(message); - error.name = "NotFoundError"; - error.__proto__ = NotFoundError.prototype; - return error; -}; -NotFoundError.prototype.__proto__ = Error.prototype; + const error = new Error(message) + error.name = 'NotFoundError' + error.__proto__ = NotFoundError.prototype + return error +} +NotFoundError.prototype.__proto__ = Error.prototype var FilesOutOfSyncError = function(message) { - const error = new Error(message); - error.name = "FilesOutOfSyncError"; - error.__proto__ = FilesOutOfSyncError.prototype; - return error; -}; -FilesOutOfSyncError.prototype.__proto__ = Error.prototype; + const error = new Error(message) + error.name = 'FilesOutOfSyncError' + error.__proto__ = FilesOutOfSyncError.prototype + return error +} +FilesOutOfSyncError.prototype.__proto__ = Error.prototype var AlreadyCompilingError = function(message) { - const error = new Error(message); - error.name = "AlreadyCompilingError"; - error.__proto__ = AlreadyCompilingError.prototype; - return error; -}; -AlreadyCompilingError.prototype.__proto__ = Error.prototype; + const error = new Error(message) + error.name = 'AlreadyCompilingError' + error.__proto__ = AlreadyCompilingError.prototype + return error +} +AlreadyCompilingError.prototype.__proto__ = Error.prototype -module.exports = (Errors = { - NotFoundError, - FilesOutOfSyncError, - AlreadyCompilingError -}); +module.exports = Errors = { + NotFoundError, + FilesOutOfSyncError, + AlreadyCompilingError +} diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index e569df8d..972f1fe7 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -13,119 +13,192 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let LatexRunner; -const Path = require("path"); -const Settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); -const Metrics = require("./Metrics"); -const CommandRunner = require("./CommandRunner"); - -const ProcessTable = {}; // table of currently running jobs (pids or docker container names) - -module.exports = (LatexRunner = { - runLatex(project_id, options, callback) { - let command; - if (callback == null) { callback = function(error) {}; } - let {directory, mainFile, compiler, timeout, image, environment, flags} = options; - if (!compiler) { compiler = "pdflatex"; } - if (!timeout) { timeout = 60000; } // milliseconds - - logger.log({directory, compiler, timeout, mainFile, environment, flags}, "starting compile"); - - // We want to run latexmk on the tex file which we will automatically - // generate from the Rtex/Rmd/md file. - mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, ".tex"); - - if (compiler === "pdflatex") { - command = LatexRunner._pdflatexCommand(mainFile, flags); - } else if (compiler === "latex") { - command = LatexRunner._latexCommand(mainFile, flags); - } else if (compiler === "xelatex") { - command = LatexRunner._xelatexCommand(mainFile, flags); - } else if (compiler === "lualatex") { - command = LatexRunner._lualatexCommand(mainFile, flags); - } else { - return callback(new Error(`unknown compiler: ${compiler}`)); - } - - if (Settings.clsi != null ? Settings.clsi.strace : undefined) { - command = ["strace", "-o", "strace", "-ff"].concat(command); - } - - const id = `${project_id}`; // record running project under this id - - return ProcessTable[id] = CommandRunner.run(project_id, command, directory, image, timeout, environment, function(error, output) { - delete ProcessTable[id]; - if (error != null) { return callback(error); } - const runs = __guard__(__guard__(output != null ? output.stderr : undefined, x1 => x1.match(/^Run number \d+ of .*latex/mg)), x => x.length) || 0; - const failed = (__guard__(output != null ? output.stdout : undefined, x2 => x2.match(/^Latexmk: Errors/m)) != null) ? 1 : 0; - // counters from latexmk output - const stats = {}; - stats["latexmk-errors"] = failed; - stats["latex-runs"] = runs; - stats["latex-runs-with-errors"] = failed ? runs : 0; - stats[`latex-runs-${runs}`] = 1; - stats[`latex-runs-with-errors-${runs}`] = failed ? 1 : 0; - // timing information from /usr/bin/time - const timings = {}; - const stderr = output != null ? output.stderr : undefined; - timings["cpu-percent"] = __guard__(stderr != null ? stderr.match(/Percent of CPU this job got: (\d+)/m) : undefined, x3 => x3[1]) || 0; - timings["cpu-time"] = __guard__(stderr != null ? stderr.match(/User time.*: (\d+.\d+)/m) : undefined, x4 => x4[1]) || 0; - timings["sys-time"] = __guard__(stderr != null ? stderr.match(/System time.*: (\d+.\d+)/m) : undefined, x5 => x5[1]) || 0; - return callback(error, output, stats, timings); - }); - }, - - killLatex(project_id, callback) { - if (callback == null) { callback = function(error) {}; } - const id = `${project_id}`; - logger.log({id}, "killing running compile"); - if ((ProcessTable[id] == null)) { - logger.warn({id}, "no such project to kill"); - return callback(null); - } else { - return CommandRunner.kill(ProcessTable[id], callback); - } - }, - - _latexmkBaseCommand(flags) { - let args = ["latexmk", "-cd", "-f", "-jobname=output", "-auxdir=$COMPILE_DIR", "-outdir=$COMPILE_DIR", "-synctex=1","-interaction=batchmode"]; - if (flags) { - args = args.concat(flags); - } - return (__guard__(Settings != null ? Settings.clsi : undefined, x => x.latexmkCommandPrefix) || []).concat(args); - }, - - _pdflatexCommand(mainFile, flags) { - return LatexRunner._latexmkBaseCommand(flags).concat([ - "-pdf", - Path.join("$COMPILE_DIR", mainFile) - ]); - }, - - _latexCommand(mainFile, flags) { - return LatexRunner._latexmkBaseCommand(flags).concat([ - "-pdfdvi", - Path.join("$COMPILE_DIR", mainFile) - ]); - }, - - _xelatexCommand(mainFile, flags) { - return LatexRunner._latexmkBaseCommand(flags).concat([ - "-xelatex", - Path.join("$COMPILE_DIR", mainFile) - ]); - }, - - _lualatexCommand(mainFile, flags) { - return LatexRunner._latexmkBaseCommand(flags).concat([ - "-lualatex", - Path.join("$COMPILE_DIR", mainFile) - ]); - } -}); - +let LatexRunner +const Path = require('path') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const Metrics = require('./Metrics') +const CommandRunner = require('./CommandRunner') + +const ProcessTable = {} // table of currently running jobs (pids or docker container names) + +module.exports = LatexRunner = { + runLatex(project_id, options, callback) { + let command + if (callback == null) { + callback = function(error) {} + } + let { + directory, + mainFile, + compiler, + timeout, + image, + environment, + flags + } = options + if (!compiler) { + compiler = 'pdflatex' + } + if (!timeout) { + timeout = 60000 + } // milliseconds + + logger.log( + { directory, compiler, timeout, mainFile, environment, flags }, + 'starting compile' + ) + + // We want to run latexmk on the tex file which we will automatically + // generate from the Rtex/Rmd/md file. + mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, '.tex') + + if (compiler === 'pdflatex') { + command = LatexRunner._pdflatexCommand(mainFile, flags) + } else if (compiler === 'latex') { + command = LatexRunner._latexCommand(mainFile, flags) + } else if (compiler === 'xelatex') { + command = LatexRunner._xelatexCommand(mainFile, flags) + } else if (compiler === 'lualatex') { + command = LatexRunner._lualatexCommand(mainFile, flags) + } else { + return callback(new Error(`unknown compiler: ${compiler}`)) + } + + if (Settings.clsi != null ? Settings.clsi.strace : undefined) { + command = ['strace', '-o', 'strace', '-ff'].concat(command) + } + + const id = `${project_id}` // record running project under this id + + return (ProcessTable[id] = CommandRunner.run( + project_id, + command, + directory, + image, + timeout, + environment, + function(error, output) { + delete ProcessTable[id] + if (error != null) { + return callback(error) + } + const runs = + __guard__( + __guard__(output != null ? output.stderr : undefined, x1 => + x1.match(/^Run number \d+ of .*latex/gm) + ), + x => x.length + ) || 0 + const failed = + __guard__(output != null ? output.stdout : undefined, x2 => + x2.match(/^Latexmk: Errors/m) + ) != null + ? 1 + : 0 + // counters from latexmk output + const stats = {} + stats['latexmk-errors'] = failed + stats['latex-runs'] = runs + stats['latex-runs-with-errors'] = failed ? runs : 0 + stats[`latex-runs-${runs}`] = 1 + stats[`latex-runs-with-errors-${runs}`] = failed ? 1 : 0 + // timing information from /usr/bin/time + const timings = {} + const stderr = output != null ? output.stderr : undefined + timings['cpu-percent'] = + __guard__( + stderr != null + ? stderr.match(/Percent of CPU this job got: (\d+)/m) + : undefined, + x3 => x3[1] + ) || 0 + timings['cpu-time'] = + __guard__( + stderr != null + ? stderr.match(/User time.*: (\d+.\d+)/m) + : undefined, + x4 => x4[1] + ) || 0 + timings['sys-time'] = + __guard__( + stderr != null + ? stderr.match(/System time.*: (\d+.\d+)/m) + : undefined, + x5 => x5[1] + ) || 0 + return callback(error, output, stats, timings) + } + )) + }, + + killLatex(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + const id = `${project_id}` + logger.log({ id }, 'killing running compile') + if (ProcessTable[id] == null) { + logger.warn({ id }, 'no such project to kill') + return callback(null) + } else { + return CommandRunner.kill(ProcessTable[id], callback) + } + }, + + _latexmkBaseCommand(flags) { + let args = [ + 'latexmk', + '-cd', + '-f', + '-jobname=output', + '-auxdir=$COMPILE_DIR', + '-outdir=$COMPILE_DIR', + '-synctex=1', + '-interaction=batchmode' + ] + if (flags) { + args = args.concat(flags) + } + return ( + __guard__( + Settings != null ? Settings.clsi : undefined, + x => x.latexmkCommandPrefix + ) || [] + ).concat(args) + }, + + _pdflatexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ + '-pdf', + Path.join('$COMPILE_DIR', mainFile) + ]) + }, + + _latexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ + '-pdfdvi', + Path.join('$COMPILE_DIR', mainFile) + ]) + }, + + _xelatexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ + '-xelatex', + Path.join('$COMPILE_DIR', mainFile) + ]) + }, + + _lualatexCommand(mainFile, flags) { + return LatexRunner._latexmkBaseCommand(flags).concat([ + '-lualatex', + Path.join('$COMPILE_DIR', mainFile) + ]) + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/app/js/LocalCommandRunner.js b/app/js/LocalCommandRunner.js index 24c0d8ea..61ecd887 100644 --- a/app/js/LocalCommandRunner.js +++ b/app/js/LocalCommandRunner.js @@ -13,62 +13,79 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let CommandRunner; -const { spawn } = require("child_process"); -const logger = require("logger-sharelatex"); +let CommandRunner +const { spawn } = require('child_process') +const logger = require('logger-sharelatex') -logger.info("using standard command runner"); +logger.info('using standard command runner') -module.exports = (CommandRunner = { - run(project_id, command, directory, image, timeout, environment, callback) { - let key, value; - if (callback == null) { callback = function(error) {}; } - command = (Array.from(command).map((arg) => arg.toString().replace('$COMPILE_DIR', directory))); - logger.log({project_id, command, directory}, "running command"); - logger.warn("timeouts and sandboxing are not enabled with CommandRunner"); +module.exports = CommandRunner = { + run(project_id, command, directory, image, timeout, environment, callback) { + let key, value + if (callback == null) { + callback = function(error) {} + } + command = Array.from(command).map(arg => + arg.toString().replace('$COMPILE_DIR', directory) + ) + logger.log({ project_id, command, directory }, 'running command') + logger.warn('timeouts and sandboxing are not enabled with CommandRunner') - // merge environment settings - const env = {}; - for (key in process.env) { value = process.env[key]; env[key] = value; } - for (key in environment) { value = environment[key]; env[key] = value; } + // merge environment settings + const env = {} + for (key in process.env) { + value = process.env[key] + env[key] = value + } + for (key in environment) { + value = environment[key] + env[key] = value + } - // run command as detached process so it has its own process group (which can be killed if needed) - const proc = spawn(command[0], command.slice(1), {cwd: directory, env}); + // run command as detached process so it has its own process group (which can be killed if needed) + const proc = spawn(command[0], command.slice(1), { cwd: directory, env }) - let stdout = ""; - proc.stdout.on("data", data=> stdout += data); + let stdout = '' + proc.stdout.on('data', data => (stdout += data)) - proc.on("error", function(err){ - logger.err({err, project_id, command, directory}, "error running command"); - return callback(err); - }); + proc.on('error', function(err) { + logger.err( + { err, project_id, command, directory }, + 'error running command' + ) + return callback(err) + }) - proc.on("close", function(code, signal) { - let err; - logger.info({code, signal, project_id}, "command exited"); - if (signal === 'SIGTERM') { // signal from kill method below - err = new Error("terminated"); - err.terminated = true; - return callback(err); - } else if (code === 1) { // exit status from chktex - err = new Error("exited"); - err.code = code; - return callback(err); - } else { - return callback(null, {"stdout": stdout}); - } - }); + proc.on('close', function(code, signal) { + let err + logger.info({ code, signal, project_id }, 'command exited') + if (signal === 'SIGTERM') { + // signal from kill method below + err = new Error('terminated') + err.terminated = true + return callback(err) + } else if (code === 1) { + // exit status from chktex + err = new Error('exited') + err.code = code + return callback(err) + } else { + return callback(null, { stdout: stdout }) + } + }) - return proc.pid; - }, // return process id to allow job to be killed if necessary + return proc.pid + }, // return process id to allow job to be killed if necessary - kill(pid, callback) { - if (callback == null) { callback = function(error) {}; } - try { - process.kill(-pid); // kill all processes in group - } catch (err) { - return callback(err); - } - return callback(); - } -}); + kill(pid, callback) { + if (callback == null) { + callback = function(error) {} + } + try { + process.kill(-pid) // kill all processes in group + } catch (err) { + return callback(err) + } + return callback() + } +} diff --git a/app/js/LockManager.js b/app/js/LockManager.js index 8930fab6..2da7da10 100644 --- a/app/js/LockManager.js +++ b/app/js/LockManager.js @@ -11,46 +11,62 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let LockManager; -const Settings = require('settings-sharelatex'); -const logger = require("logger-sharelatex"); -const Lockfile = require('lockfile'); // from https://github.com/npm/lockfile -const Errors = require("./Errors"); -const fs = require("fs"); -const Path = require("path"); -module.exports = (LockManager = { - LOCK_TEST_INTERVAL: 1000, // 50ms between each test of the lock - MAX_LOCK_WAIT_TIME: 15000, // 10s maximum time to spend trying to get the lock - LOCK_STALE: 5*60*1000, // 5 mins time until lock auto expires +let LockManager +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const Lockfile = require('lockfile') // from https://github.com/npm/lockfile +const Errors = require('./Errors') +const fs = require('fs') +const Path = require('path') +module.exports = LockManager = { + LOCK_TEST_INTERVAL: 1000, // 50ms between each test of the lock + MAX_LOCK_WAIT_TIME: 15000, // 10s maximum time to spend trying to get the lock + LOCK_STALE: 5 * 60 * 1000, // 5 mins time until lock auto expires - runWithLock(path, runner, callback) { - if (callback == null) { callback = function(error) {}; } - const lockOpts = { - wait: this.MAX_LOCK_WAIT_TIME, - pollPeriod: this.LOCK_TEST_INTERVAL, - stale: this.LOCK_STALE - }; - return Lockfile.lock(path, lockOpts, function(error) { - if ((error != null ? error.code : undefined) === 'EEXIST') { - return callback(new Errors.AlreadyCompilingError("compile in progress")); - } else if (error != null) { - return fs.lstat(path, (statLockErr, statLock)=> - fs.lstat(Path.dirname(path), (statDirErr, statDir)=> - fs.readdir(Path.dirname(path), function(readdirErr, readdirDir){ - logger.err({error, path, statLock, statLockErr, statDir, statDirErr, readdirErr, readdirDir}, "unable to get lock"); - return callback(error); - }) - ) - ); - } else { - return runner((error1, ...args) => - Lockfile.unlock(path, function(error2) { - error = error1 || error2; - if (error != null) { return callback(error); } - return callback(null, ...Array.from(args)); - }) - ); - } - }); - } -}); + runWithLock(path, runner, callback) { + if (callback == null) { + callback = function(error) {} + } + const lockOpts = { + wait: this.MAX_LOCK_WAIT_TIME, + pollPeriod: this.LOCK_TEST_INTERVAL, + stale: this.LOCK_STALE + } + return Lockfile.lock(path, lockOpts, function(error) { + if ((error != null ? error.code : undefined) === 'EEXIST') { + return callback(new Errors.AlreadyCompilingError('compile in progress')) + } else if (error != null) { + return fs.lstat(path, (statLockErr, statLock) => + fs.lstat(Path.dirname(path), (statDirErr, statDir) => + fs.readdir(Path.dirname(path), function(readdirErr, readdirDir) { + logger.err( + { + error, + path, + statLock, + statLockErr, + statDir, + statDirErr, + readdirErr, + readdirDir + }, + 'unable to get lock' + ) + return callback(error) + }) + ) + ) + } else { + return runner((error1, ...args) => + Lockfile.unlock(path, function(error2) { + error = error1 || error2 + if (error != null) { + return callback(error) + } + return callback(null, ...Array.from(args)) + }) + ) + } + }) + } +} diff --git a/app/js/Metrics.js b/app/js/Metrics.js index 94623da3..e9676415 100644 --- a/app/js/Metrics.js +++ b/app/js/Metrics.js @@ -1,4 +1,3 @@ // TODO: This file was created by bulk-decaffeinate. // Sanity-check the conversion and remove this comment. -module.exports = require("metrics-sharelatex"); - +module.exports = require('metrics-sharelatex') diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index b1bda0e8..c2c962f1 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -13,263 +13,387 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let OutputCacheManager; -const async = require("async"); -const fs = require("fs"); -const fse = require("fs-extra"); -const Path = require("path"); -const logger = require("logger-sharelatex"); -const _ = require("underscore"); -const Settings = require("settings-sharelatex"); -const crypto = require("crypto"); +let OutputCacheManager +const async = require('async') +const fs = require('fs') +const fse = require('fs-extra') +const Path = require('path') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const Settings = require('settings-sharelatex') +const crypto = require('crypto') -const OutputFileOptimiser = require("./OutputFileOptimiser"); +const OutputFileOptimiser = require('./OutputFileOptimiser') -module.exports = (OutputCacheManager = { - CACHE_SUBDIR: '.cache/clsi', - ARCHIVE_SUBDIR: '.archive/clsi', - // build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes - // for backwards compatibility, make the randombytes part optional - BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/, - CACHE_LIMIT: 2, // maximum number of cache directories - CACHE_AGE: 60*60*1000, // up to one hour old +module.exports = OutputCacheManager = { + CACHE_SUBDIR: '.cache/clsi', + ARCHIVE_SUBDIR: '.archive/clsi', + // build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes + // for backwards compatibility, make the randombytes part optional + BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/, + CACHE_LIMIT: 2, // maximum number of cache directories + CACHE_AGE: 60 * 60 * 1000, // up to one hour old - path(buildId, file) { - // used by static server, given build id return '.cache/clsi/buildId' - if (buildId.match(OutputCacheManager.BUILD_REGEX)) { - return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file); - } else { - // for invalid build id, return top level - return file; - } - }, + path(buildId, file) { + // used by static server, given build id return '.cache/clsi/buildId' + if (buildId.match(OutputCacheManager.BUILD_REGEX)) { + return Path.join(OutputCacheManager.CACHE_SUBDIR, buildId, file) + } else { + // for invalid build id, return top level + return file + } + }, - generateBuildId(callback) { - // generate a secure build id from Date.now() and 8 random bytes in hex - if (callback == null) { callback = function(error, buildId) {}; } - return crypto.randomBytes(8, function(err, buf) { - if (err != null) { return callback(err); } - const random = buf.toString('hex'); - const date = Date.now().toString(16); - return callback(err, `${date}-${random}`); - }); - }, + generateBuildId(callback) { + // generate a secure build id from Date.now() and 8 random bytes in hex + if (callback == null) { + callback = function(error, buildId) {} + } + return crypto.randomBytes(8, function(err, buf) { + if (err != null) { + return callback(err) + } + const random = buf.toString('hex') + const date = Date.now().toString(16) + return callback(err, `${date}-${random}`) + }) + }, - saveOutputFiles(outputFiles, compileDir, callback) { - if (callback == null) { callback = function(error) {}; } - return OutputCacheManager.generateBuildId(function(err, buildId) { - if (err != null) { return callback(err); } - return OutputCacheManager.saveOutputFilesInBuildDir(outputFiles, compileDir, buildId, callback); - }); - }, + saveOutputFiles(outputFiles, compileDir, callback) { + if (callback == null) { + callback = function(error) {} + } + return OutputCacheManager.generateBuildId(function(err, buildId) { + if (err != null) { + return callback(err) + } + return OutputCacheManager.saveOutputFilesInBuildDir( + outputFiles, + compileDir, + buildId, + callback + ) + }) + }, - saveOutputFilesInBuildDir(outputFiles, compileDir, buildId, callback) { - // make a compileDir/CACHE_SUBDIR/build_id directory and - // copy all the output files into it - if (callback == null) { callback = function(error) {}; } - const cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR); - // Put the files into a new cache subdirectory - const cacheDir = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR, buildId); - // Is it a per-user compile? check if compile directory is PROJECTID-USERID - const perUser = Path.basename(compileDir).match(/^[0-9a-f]{24}-[0-9a-f]{24}$/); + saveOutputFilesInBuildDir(outputFiles, compileDir, buildId, callback) { + // make a compileDir/CACHE_SUBDIR/build_id directory and + // copy all the output files into it + if (callback == null) { + callback = function(error) {} + } + const cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR) + // Put the files into a new cache subdirectory + const cacheDir = Path.join( + compileDir, + OutputCacheManager.CACHE_SUBDIR, + buildId + ) + // Is it a per-user compile? check if compile directory is PROJECTID-USERID + const perUser = Path.basename(compileDir).match( + /^[0-9a-f]{24}-[0-9a-f]{24}$/ + ) - // Archive logs in background - if ((Settings.clsi != null ? Settings.clsi.archive_logs : undefined) || (Settings.clsi != null ? Settings.clsi.strace : undefined)) { - OutputCacheManager.archiveLogs(outputFiles, compileDir, buildId, function(err) { - if (err != null) { - return logger.warn({err}, "erroring archiving log files"); - } - }); - } + // Archive logs in background + if ( + (Settings.clsi != null ? Settings.clsi.archive_logs : undefined) || + (Settings.clsi != null ? Settings.clsi.strace : undefined) + ) { + OutputCacheManager.archiveLogs(outputFiles, compileDir, buildId, function( + err + ) { + if (err != null) { + return logger.warn({ err }, 'erroring archiving log files') + } + }) + } - // make the new cache directory - return fse.ensureDir(cacheDir, function(err) { - if (err != null) { - logger.error({err, directory: cacheDir}, "error creating cache directory"); - return callback(err, outputFiles); - } else { - // copy all the output files into the new cache directory - const results = []; - return async.mapSeries(outputFiles, function(file, cb) { - // don't send dot files as output, express doesn't serve them - if (OutputCacheManager._fileIsHidden(file.path)) { - logger.debug({compileDir, path: file.path}, "ignoring dotfile in output"); - return cb(); - } - // copy other files into cache directory if valid - const newFile = _.clone(file); - const [src, dst] = Array.from([Path.join(compileDir, file.path), Path.join(cacheDir, file.path)]); - return OutputCacheManager._checkFileIsSafe(src, function(err, isSafe) { - if (err != null) { return cb(err); } - if (!isSafe) { - return cb(); - } - return OutputCacheManager._checkIfShouldCopy(src, function(err, shouldCopy) { - if (err != null) { return cb(err); } - if (!shouldCopy) { - return cb(); - } - return OutputCacheManager._copyFile(src, dst, function(err) { - if (err != null) { return cb(err); } - newFile.build = buildId; // attach a build id if we cached the file - results.push(newFile); - return cb(); - }); - }); - }); - } - , function(err) { - if (err != null) { - // pass back the original files if we encountered *any* error - callback(err, outputFiles); - // clean up the directory we just created - return fse.remove(cacheDir, function(err) { - if (err != null) { - return logger.error({err, dir: cacheDir}, "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 - return OutputCacheManager.expireOutputFiles(cacheRoot, {keep: buildId, limit: perUser ? 1 : null}); - } - }); - } - }); - }, + // make the new cache directory + return fse.ensureDir(cacheDir, function(err) { + if (err != null) { + logger.error( + { err, directory: cacheDir }, + 'error creating cache directory' + ) + return callback(err, outputFiles) + } else { + // copy all the output files into the new cache directory + const results = [] + return async.mapSeries( + outputFiles, + function(file, cb) { + // don't send dot files as output, express doesn't serve them + if (OutputCacheManager._fileIsHidden(file.path)) { + logger.debug( + { compileDir, path: file.path }, + 'ignoring dotfile in output' + ) + return cb() + } + // copy other files into cache directory if valid + const newFile = _.clone(file) + const [src, dst] = Array.from([ + Path.join(compileDir, file.path), + Path.join(cacheDir, file.path) + ]) + return OutputCacheManager._checkFileIsSafe(src, function( + err, + isSafe + ) { + if (err != null) { + return cb(err) + } + if (!isSafe) { + return cb() + } + return OutputCacheManager._checkIfShouldCopy(src, function( + err, + shouldCopy + ) { + if (err != null) { + return cb(err) + } + if (!shouldCopy) { + return cb() + } + return OutputCacheManager._copyFile(src, dst, function(err) { + if (err != null) { + return cb(err) + } + newFile.build = buildId // attach a build id if we cached the file + results.push(newFile) + return cb() + }) + }) + }) + }, + function(err) { + if (err != null) { + // pass back the original files if we encountered *any* error + callback(err, outputFiles) + // clean up the directory we just created + return fse.remove(cacheDir, function(err) { + if (err != null) { + return logger.error( + { err, dir: cacheDir }, + '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 + return OutputCacheManager.expireOutputFiles(cacheRoot, { + keep: buildId, + limit: perUser ? 1 : null + }) + } + } + ) + } + }) + }, - archiveLogs(outputFiles, compileDir, buildId, callback) { - if (callback == null) { callback = function(error) {}; } - const archiveDir = Path.join(compileDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId); - logger.log({dir: archiveDir}, "archiving log files for project"); - return fse.ensureDir(archiveDir, function(err) { - if (err != null) { return callback(err); } - return async.mapSeries(outputFiles, function(file, cb) { - const [src, dst] = Array.from([Path.join(compileDir, file.path), Path.join(archiveDir, file.path)]); - return OutputCacheManager._checkFileIsSafe(src, function(err, isSafe) { - if (err != null) { return cb(err); } - if (!isSafe) { return cb(); } - return OutputCacheManager._checkIfShouldArchive(src, function(err, shouldArchive) { - if (err != null) { return cb(err); } - if (!shouldArchive) { return cb(); } - return OutputCacheManager._copyFile(src, dst, cb); - }); - }); - } - , callback); - }); - }, + archiveLogs(outputFiles, compileDir, buildId, callback) { + if (callback == null) { + callback = function(error) {} + } + const archiveDir = Path.join( + compileDir, + OutputCacheManager.ARCHIVE_SUBDIR, + buildId + ) + logger.log({ dir: archiveDir }, 'archiving log files for project') + return fse.ensureDir(archiveDir, function(err) { + if (err != null) { + return callback(err) + } + return async.mapSeries( + outputFiles, + function(file, cb) { + const [src, dst] = Array.from([ + Path.join(compileDir, file.path), + Path.join(archiveDir, file.path) + ]) + return OutputCacheManager._checkFileIsSafe(src, function( + err, + isSafe + ) { + if (err != null) { + return cb(err) + } + if (!isSafe) { + return cb() + } + return OutputCacheManager._checkIfShouldArchive(src, function( + err, + shouldArchive + ) { + if (err != null) { + return cb(err) + } + if (!shouldArchive) { + return cb() + } + return OutputCacheManager._copyFile(src, dst, cb) + }) + }) + }, + callback + ) + }) + }, - expireOutputFiles(cacheRoot, options, callback) { - // look in compileDir for build dirs and delete if > N or age of mod time > T - if (callback == null) { callback = function(error) {}; } - return fs.readdir(cacheRoot, function(err, results) { - if (err != null) { - if (err.code === 'ENOENT') { return callback(null); } // cache directory is empty - logger.error({err, project_id: cacheRoot}, "error clearing cache"); - return callback(err); - } + expireOutputFiles(cacheRoot, options, callback) { + // look in compileDir for build dirs and delete if > N or age of mod time > T + if (callback == null) { + callback = function(error) {} + } + return fs.readdir(cacheRoot, function(err, results) { + if (err != null) { + if (err.code === 'ENOENT') { + return callback(null) + } // cache directory is empty + logger.error({ err, project_id: cacheRoot }, 'error clearing cache') + return callback(err) + } - const dirs = results.sort().reverse(); - const currentTime = Date.now(); + const dirs = results.sort().reverse() + const currentTime = Date.now() - const isExpired = function(dir, index) { - if ((options != null ? options.keep : undefined) === dir) { return false; } - // remove any directories over the requested (non-null) limit - if (((options != null ? options.limit : undefined) != null) && (index > options.limit)) { return true; } - // remove any directories over the hard limit - if (index > OutputCacheManager.CACHE_LIMIT) { return true; } - // we can get the build time from the first part of the directory name DDDD-RRRR - // DDDD is date and RRRR is random bytes - const dirTime = parseInt(__guard__(dir.split('-'), x => x[0]), 16); - const age = currentTime - dirTime; - return age > OutputCacheManager.CACHE_AGE; - }; + const isExpired = function(dir, index) { + if ((options != null ? options.keep : undefined) === dir) { + return false + } + // remove any directories over the requested (non-null) limit + if ( + (options != null ? options.limit : undefined) != null && + index > options.limit + ) { + return true + } + // remove any directories over the hard limit + if (index > OutputCacheManager.CACHE_LIMIT) { + return true + } + // we can get the build time from the first part of the directory name DDDD-RRRR + // DDDD is date and RRRR is random bytes + const dirTime = parseInt( + __guard__(dir.split('-'), x => x[0]), + 16 + ) + const age = currentTime - dirTime + return age > OutputCacheManager.CACHE_AGE + } - const toRemove = _.filter(dirs, isExpired); + const toRemove = _.filter(dirs, isExpired) - const removeDir = (dir, cb) => - fse.remove(Path.join(cacheRoot, dir), function(err, result) { - logger.log({cache: cacheRoot, dir}, "removed expired cache dir"); - if (err != null) { - logger.error({err, dir}, "cache remove error"); - } - return cb(err, result); - }) - ; + const removeDir = (dir, cb) => + fse.remove(Path.join(cacheRoot, dir), function(err, result) { + logger.log({ cache: cacheRoot, dir }, 'removed expired cache dir') + if (err != null) { + logger.error({ err, dir }, 'cache remove error') + } + return cb(err, result) + }) + return async.eachSeries( + toRemove, + (dir, cb) => removeDir(dir, cb), + callback + ) + }) + }, - return async.eachSeries(toRemove, (dir, cb) => removeDir(dir, cb) - , callback); - }); - }, + _fileIsHidden(path) { + return (path != null ? path.match(/^\.|\/\./) : undefined) != null + }, - _fileIsHidden(path) { - return ((path != null ? path.match(/^\.|\/\./) : undefined) != null); - }, + _checkFileIsSafe(src, callback) { + // check if we have a valid file to copy into the cache + if (callback == null) { + callback = function(error, isSafe) {} + } + return fs.stat(src, function(err, stats) { + if ((err != null ? err.code : undefined) === 'ENOENT') { + logger.warn( + { err, file: src }, + 'file has disappeared before copying to build cache' + ) + return callback(err, false) + } else if (err != null) { + // some other problem reading the file + logger.error({ err, file: src }, 'stat error for file in cache') + return callback(err, false) + } else if (!stats.isFile()) { + // other filetype - reject it + logger.warn( + { src, stat: stats }, + 'nonfile output - refusing to copy to cache' + ) + return callback(null, false) + } else { + // it's a plain file, ok to copy + return callback(null, true) + } + }) + }, - _checkFileIsSafe(src, callback) { - // check if we have a valid file to copy into the cache - if (callback == null) { callback = function(error, isSafe) {}; } - return fs.stat(src, function(err, stats) { - if ((err != null ? err.code : undefined) === 'ENOENT') { - logger.warn({err, file: src}, "file has disappeared before copying to build cache"); - return callback(err, false); - } else if (err != null) { - // some other problem reading the file - logger.error({err, file: src}, "stat error for file in cache"); - return callback(err, false); - } else if (!stats.isFile()) { - // other filetype - reject it - logger.warn({src, stat: stats}, "nonfile output - refusing to copy to cache"); - return callback(null, false); - } else { - // it's a plain file, ok to copy - return callback(null, true); - } - }); - }, + _copyFile(src, dst, callback) { + // copy output file into the cache + return fse.copy(src, dst, function(err) { + if ((err != null ? err.code : undefined) === 'ENOENT') { + logger.warn( + { err, file: src }, + 'file has disappeared when copying to build cache' + ) + return callback(err, false) + } else if (err != null) { + logger.error({ err, src, dst }, 'copy error for file in cache') + return callback(err) + } else { + if ( + Settings.clsi != null ? Settings.clsi.optimiseInDocker : undefined + ) { + // don't run any optimisations on the pdf when they are done + // in the docker container + return callback() + } else { + // call the optimiser for the file too + return OutputFileOptimiser.optimiseFile(src, dst, callback) + } + } + }) + }, - _copyFile(src, dst, callback) { - // copy output file into the cache - return fse.copy(src, dst, function(err) { - if ((err != null ? err.code : undefined) === 'ENOENT') { - logger.warn({err, file: src}, "file has disappeared when copying to build cache"); - return callback(err, false); - } else if (err != null) { - logger.error({err, src, dst}, "copy error for file in cache"); - return callback(err); - } else { - if ((Settings.clsi != null ? Settings.clsi.optimiseInDocker : undefined)) { - // don't run any optimisations on the pdf when they are done - // in the docker container - return callback(); - } else { - // call the optimiser for the file too - return OutputFileOptimiser.optimiseFile(src, dst, callback); - } - } - }); - }, + _checkIfShouldCopy(src, callback) { + if (callback == null) { + callback = function(err, shouldCopy) {} + } + return callback(null, !Path.basename(src).match(/^strace/)) + }, - _checkIfShouldCopy(src, callback) { - if (callback == null) { callback = function(err, shouldCopy) {}; } - return callback(null, !Path.basename(src).match(/^strace/)); - }, - - _checkIfShouldArchive(src, callback) { - let needle; - if (callback == null) { callback = function(err, shouldCopy) {}; } - if (Path.basename(src).match(/^strace/)) { - return callback(null, true); - } - if ((Settings.clsi != null ? Settings.clsi.archive_logs : undefined) && (needle = Path.basename(src), ["output.log", "output.blg"].includes(needle))) { - return callback(null, true); - } - return callback(null, false); - } -}); + _checkIfShouldArchive(src, callback) { + let needle + if (callback == null) { + callback = function(err, shouldCopy) {} + } + if (Path.basename(src).match(/^strace/)) { + return callback(null, true) + } + if ( + (Settings.clsi != null ? Settings.clsi.archive_logs : undefined) && + ((needle = Path.basename(src)), + ['output.log', 'output.blg'].includes(needle)) + ) { + return callback(null, true) + } + return callback(null, false) + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 21a75874..50012b51 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -14,73 +14,102 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let OutputFileFinder; -const async = require("async"); -const fs = require("fs"); -const Path = require("path"); -const { spawn } = require("child_process"); -const logger = require("logger-sharelatex"); +let OutputFileFinder +const async = require('async') +const fs = require('fs') +const Path = require('path') +const { spawn } = require('child_process') +const logger = require('logger-sharelatex') -module.exports = (OutputFileFinder = { - findOutputFiles(resources, directory, callback) { - if (callback == null) { callback = function(error, outputFiles, allFiles) {}; } - const incomingResources = {}; - for (const resource of Array.from(resources)) { - incomingResources[resource.path] = true; - } - - return OutputFileFinder._getAllFiles(directory, function(error, allFiles) { - if (allFiles == null) { allFiles = []; } - if (error != null) { - logger.err({err:error}, "error finding all output files"); - return callback(error); - } - const outputFiles = []; - for (const file of Array.from(allFiles)) { - if (!incomingResources[file]) { - outputFiles.push({ - path: file, - type: __guard__(file.match(/\.([^\.]+)$/), x => x[1]) - }); - } - } - return callback(null, outputFiles, allFiles); - }); - }, +module.exports = OutputFileFinder = { + findOutputFiles(resources, directory, callback) { + if (callback == null) { + callback = function(error, outputFiles, allFiles) {} + } + const incomingResources = {} + for (const resource of Array.from(resources)) { + incomingResources[resource.path] = true + } - _getAllFiles(directory, _callback) { - if (_callback == null) { _callback = function(error, fileList) {}; } - const callback = function(error, fileList) { - _callback(error, fileList); - return _callback = function() {}; - }; + return OutputFileFinder._getAllFiles(directory, function(error, allFiles) { + if (allFiles == null) { + allFiles = [] + } + if (error != null) { + logger.err({ err: error }, 'error finding all output files') + return callback(error) + } + const outputFiles = [] + for (const file of Array.from(allFiles)) { + if (!incomingResources[file]) { + outputFiles.push({ + path: file, + type: __guard__(file.match(/\.([^\.]+)$/), x => x[1]) + }) + } + } + return callback(null, outputFiles, allFiles) + }) + }, - // don't include clsi-specific files/directories in the output list - const EXCLUDE_DIRS = ["-name", ".cache", "-o", "-name", ".archive","-o", "-name", ".project-*"]; - const args = [directory, "(", ...Array.from(EXCLUDE_DIRS), ")", "-prune", "-o", "-type", "f", "-print"]; - logger.log({args}, "running find command"); + _getAllFiles(directory, _callback) { + if (_callback == null) { + _callback = function(error, fileList) {} + } + const callback = function(error, fileList) { + _callback(error, fileList) + return (_callback = function() {}) + } - const proc = spawn("find", args); - let stdout = ""; - proc.stdout.on("data", chunk => stdout += chunk.toString()); - proc.on("error", callback); - return proc.on("close", function(code) { - if (code !== 0) { - logger.warn({directory, code}, "find returned error, directory likely doesn't exist"); - return callback(null, []); - } - let fileList = stdout.trim().split("\n"); - fileList = fileList.map(function(file) { - // Strip leading directory - let path; - return path = Path.relative(directory, file); - }); - return callback(null, fileList); - }); - } -}); + // don't include clsi-specific files/directories in the output list + const EXCLUDE_DIRS = [ + '-name', + '.cache', + '-o', + '-name', + '.archive', + '-o', + '-name', + '.project-*' + ] + const args = [ + directory, + '(', + ...Array.from(EXCLUDE_DIRS), + ')', + '-prune', + '-o', + '-type', + 'f', + '-print' + ] + logger.log({ args }, 'running find command') + const proc = spawn('find', args) + let stdout = '' + proc.stdout.on('data', chunk => (stdout += chunk.toString())) + proc.on('error', callback) + return proc.on('close', function(code) { + if (code !== 0) { + logger.warn( + { directory, code }, + "find returned error, directory likely doesn't exist" + ) + return callback(null, []) + } + let fileList = stdout.trim().split('\n') + fileList = fileList.map(function(file) { + // Strip leading directory + let path + return (path = Path.relative(directory, file)) + }) + return callback(null, fileList) + }) + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/app/js/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js index 149d3846..c0b8cc14 100644 --- a/app/js/OutputFileOptimiser.js +++ b/app/js/OutputFileOptimiser.js @@ -13,74 +13,92 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let OutputFileOptimiser; -const fs = require("fs"); -const Path = require("path"); -const { spawn } = require("child_process"); -const logger = require("logger-sharelatex"); -const Metrics = require("./Metrics"); -const _ = require("underscore"); +let OutputFileOptimiser +const fs = require('fs') +const Path = require('path') +const { spawn } = require('child_process') +const logger = require('logger-sharelatex') +const Metrics = require('./Metrics') +const _ = require('underscore') -module.exports = (OutputFileOptimiser = { +module.exports = OutputFileOptimiser = { + optimiseFile(src, dst, callback) { + // check output file (src) and see if we can optimise it, storing + // the result in the build directory (dst) + if (callback == null) { + callback = function(error) {} + } + if (src.match(/\/output\.pdf$/)) { + return OutputFileOptimiser.checkIfPDFIsOptimised(src, function( + err, + isOptimised + ) { + if (err != null || isOptimised) { + return callback(null) + } + return OutputFileOptimiser.optimisePDF(src, dst, callback) + }) + } else { + return callback(null) + } + }, - optimiseFile(src, dst, callback) { - // check output file (src) and see if we can optimise it, storing - // the result in the build directory (dst) - if (callback == null) { callback = function(error) {}; } - if (src.match(/\/output\.pdf$/)) { - return OutputFileOptimiser.checkIfPDFIsOptimised(src, function(err, isOptimised) { - if ((err != null) || isOptimised) { return callback(null); } - return OutputFileOptimiser.optimisePDF(src, dst, callback); - }); - } else { - return callback((null)); - } - }, + checkIfPDFIsOptimised(file, callback) { + const SIZE = 16 * 1024 // check the header of the pdf + const result = new Buffer(SIZE) + result.fill(0) // prevent leakage of uninitialised buffer + return fs.open(file, 'r', function(err, fd) { + if (err != null) { + return callback(err) + } + return fs.read(fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) => + fs.close(fd, function(errClose) { + if (errRead != null) { + return callback(errRead) + } + if (typeof errReadClose !== 'undefined' && errReadClose !== null) { + return callback(errClose) + } + const isOptimised = + buffer.toString('ascii').indexOf('/Linearized 1') >= 0 + return callback(null, isOptimised) + }) + ) + }) + }, - checkIfPDFIsOptimised(file, callback) { - const SIZE = 16*1024; // check the header of the pdf - const result = new Buffer(SIZE); - result.fill(0); // prevent leakage of uninitialised buffer - return fs.open(file, "r", function(err, fd) { - if (err != null) { return callback(err); } - return fs.read(fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) => - fs.close(fd, function(errClose) { - if (errRead != null) { return callback(errRead); } - if (typeof errReadClose !== 'undefined' && errReadClose !== null) { return callback(errClose); } - const isOptimised = buffer.toString('ascii').indexOf("/Linearized 1") >= 0; - return callback(null, isOptimised); - }) - ); - }); - }, + optimisePDF(src, dst, callback) { + if (callback == null) { + callback = function(error) {} + } + const tmpOutput = dst + '.opt' + const args = ['--linearize', src, tmpOutput] + logger.log({ args }, 'running qpdf command') - optimisePDF(src, dst, callback) { - if (callback == null) { callback = function(error) {}; } - const tmpOutput = dst + '.opt'; - const args = ["--linearize", src, tmpOutput]; - logger.log({args}, "running qpdf command"); - - const timer = new Metrics.Timer("qpdf"); - const proc = spawn("qpdf", args); - let stdout = ""; - proc.stdout.on("data", chunk => stdout += chunk.toString()); - callback = _.once(callback); // avoid double call back for error and close event - proc.on("error", function(err) { - logger.warn({err, args}, "qpdf failed"); - return callback(null); - }); // ignore the error - return proc.on("close", function(code) { - timer.done(); - if (code !== 0) { - logger.warn({code, args}, "qpdf returned error"); - return callback(null); // ignore the error - } - return fs.rename(tmpOutput, dst, function(err) { - if (err != null) { - logger.warn({tmpOutput, dst}, "failed to rename output of qpdf command"); - } - return callback(null); - }); - }); - } // ignore the error -}); + const timer = new Metrics.Timer('qpdf') + const proc = spawn('qpdf', args) + let stdout = '' + proc.stdout.on('data', chunk => (stdout += chunk.toString())) + callback = _.once(callback) // avoid double call back for error and close event + proc.on('error', function(err) { + logger.warn({ err, args }, 'qpdf failed') + return callback(null) + }) // ignore the error + return proc.on('close', function(code) { + timer.done() + if (code !== 0) { + logger.warn({ code, args }, 'qpdf returned error') + return callback(null) // ignore the error + } + return fs.rename(tmpOutput, dst, function(err) { + if (err != null) { + logger.warn( + { tmpOutput, dst }, + 'failed to rename output of qpdf command' + ) + } + return callback(null) + }) + }) + } // ignore the error +} diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index 856c1566..8015baa9 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -11,113 +11,153 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let ProjectPersistenceManager; -const UrlCache = require("./UrlCache"); -const CompileManager = require("./CompileManager"); -const db = require("./db"); -const dbQueue = require("./DbQueue"); -const async = require("async"); -const logger = require("logger-sharelatex"); -const oneDay = 24 * 60 * 60 * 1000; -const Settings = require("settings-sharelatex"); +let ProjectPersistenceManager +const UrlCache = require('./UrlCache') +const CompileManager = require('./CompileManager') +const db = require('./db') +const dbQueue = require('./DbQueue') +const async = require('async') +const logger = require('logger-sharelatex') +const oneDay = 24 * 60 * 60 * 1000 +const Settings = require('settings-sharelatex') -module.exports = (ProjectPersistenceManager = { +module.exports = ProjectPersistenceManager = { + EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5, - EXPIRY_TIMEOUT: Settings.project_cache_length_ms || (oneDay * 2.5), + markProjectAsJustAccessed(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + const job = cb => + db.Project.findOrCreate({ where: { project_id } }) + .spread((project, created) => + project + .updateAttributes({ lastAccessed: new Date() }) + .then(() => cb()) + .error(cb) + ) + .error(cb) + return dbQueue.queue.push(job, callback) + }, - markProjectAsJustAccessed(project_id, callback) { - if (callback == null) { callback = function(error) {}; } - const job = cb=> - db.Project.findOrCreate({where: {project_id}}) - .spread( - (project, created) => - project.updateAttributes({lastAccessed: new Date()}) - .then(() => cb()) - .error(cb) - ) - .error(cb) - ; - return dbQueue.queue.push(job, callback); - }, + clearExpiredProjects(callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectPersistenceManager._findExpiredProjectIds(function( + error, + project_ids + ) { + if (error != null) { + return callback(error) + } + logger.log({ project_ids }, 'clearing expired projects') + const jobs = Array.from(project_ids || []).map(project_id => + (project_id => callback => + ProjectPersistenceManager.clearProjectFromCache(project_id, function( + err + ) { + if (err != null) { + logger.error({ err, project_id }, 'error clearing project') + } + return callback() + }))(project_id) + ) + return async.series(jobs, function(error) { + if (error != null) { + return callback(error) + } + return CompileManager.clearExpiredProjects( + ProjectPersistenceManager.EXPIRY_TIMEOUT, + error => callback() + ) + }) + }) + }, // ignore any errors from deleting directories + clearProject(project_id, user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id, user_id }, 'clearing project for user') + return CompileManager.clearProject(project_id, user_id, function(error) { + if (error != null) { + return callback(error) + } + return ProjectPersistenceManager.clearProjectFromCache( + project_id, + function(error) { + if (error != null) { + return callback(error) + } + return callback() + } + ) + }) + }, - clearExpiredProjects(callback) { - if (callback == null) { callback = function(error) {}; } - return ProjectPersistenceManager._findExpiredProjectIds(function(error, project_ids) { - if (error != null) { return callback(error); } - logger.log({project_ids}, "clearing expired projects"); - const jobs = (Array.from(project_ids || [])).map((project_id) => - (project_id => - callback => - ProjectPersistenceManager.clearProjectFromCache(project_id, function(err) { - if (err != null) { - logger.error({err, project_id}, "error clearing project"); - } - return callback(); - }) - - )(project_id)); - return async.series(jobs, function(error) { - if (error != null) { return callback(error); } - return CompileManager.clearExpiredProjects(ProjectPersistenceManager.EXPIRY_TIMEOUT, error => callback()); - }); - }); - }, // ignore any errors from deleting directories + clearProjectFromCache(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id }, 'clearing project from cache') + return UrlCache.clearProject(project_id, function(error) { + if (error != null) { + logger.err({ error, project_id }, 'error clearing project from cache') + return callback(error) + } + return ProjectPersistenceManager._clearProjectFromDatabase( + project_id, + function(error) { + if (error != null) { + logger.err( + { error, project_id }, + 'error clearing project from database' + ) + } + return callback(error) + } + ) + }) + }, - clearProject(project_id, user_id, callback) { - if (callback == null) { callback = function(error) {}; } - logger.log({project_id, user_id}, "clearing project for user"); - return CompileManager.clearProject(project_id, user_id, function(error) { - if (error != null) { return callback(error); } - return ProjectPersistenceManager.clearProjectFromCache(project_id, function(error) { - if (error != null) { return callback(error); } - return callback(); - }); - }); - }, + _clearProjectFromDatabase(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id }, 'clearing project from database') + const job = cb => + db.Project.destroy({ where: { project_id } }) + .then(() => cb()) + .error(cb) + return dbQueue.queue.push(job, callback) + }, - clearProjectFromCache(project_id, callback) { - if (callback == null) { callback = function(error) {}; } - logger.log({project_id}, "clearing project from cache"); - return UrlCache.clearProject(project_id, function(error) { - if (error != null) { - logger.err({error, project_id}, "error clearing project from cache"); - return callback(error); - } - return ProjectPersistenceManager._clearProjectFromDatabase(project_id, function(error) { - if (error != null) { - logger.err({error, project_id}, "error clearing project from database"); - } - return callback(error); - }); - }); - }, + _findExpiredProjectIds(callback) { + if (callback == null) { + callback = function(error, project_ids) {} + } + const job = function(cb) { + const keepProjectsFrom = new Date( + Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT + ) + const q = {} + q[db.op.lt] = keepProjectsFrom + return db.Project.findAll({ where: { lastAccessed: q } }) + .then(projects => + cb( + null, + projects.map(project => project.project_id) + ) + ) + .error(cb) + } - _clearProjectFromDatabase(project_id, callback) { - if (callback == null) { callback = function(error) {}; } - logger.log({project_id}, "clearing project from database"); - const job = cb=> - db.Project.destroy({where: {project_id}}) - .then(() => cb()) - .error(cb) - ; - return dbQueue.queue.push(job, callback); - }, + return dbQueue.queue.push(job, callback) + } +} - - _findExpiredProjectIds(callback) { - if (callback == null) { callback = function(error, project_ids) {}; } - const job = function(cb){ - const keepProjectsFrom = new Date(Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT); - const q = {}; - q[db.op.lt] = keepProjectsFrom; - return db.Project.findAll({where:{lastAccessed:q}}) - .then(projects => cb(null, projects.map(project => project.project_id))).error(cb); - }; - - return dbQueue.queue.push(job, callback); - } -}); - - -logger.log({EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout"); +logger.log( + { EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT }, + 'project assets kept timeout' +) diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index 6641086a..acfdc668 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -17,177 +17,201 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let RequestParser; -const settings = require("settings-sharelatex"); - -module.exports = (RequestParser = { - VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"], - MAX_TIMEOUT: 600, - - parse(body, callback) { - let resource; - if (callback == null) { callback = function(error, data) {}; } - const response = {}; - - if ((body.compile == null)) { - return callback("top level object should have a compile attribute"); - } - - const { compile } = body; - if (!compile.options) { compile.options = {}; } - - try { - response.compiler = this._parseAttribute("compiler", - compile.options.compiler, { - validValues: this.VALID_COMPILERS, - default: "pdflatex", - type: "string" - } - ); - response.timeout = this._parseAttribute("timeout", - compile.options.timeout, { - default: RequestParser.MAX_TIMEOUT, - type: "number" - } - ); - response.imageName = this._parseAttribute("imageName", - compile.options.imageName, - {type: "string"}); - response.draft = this._parseAttribute("draft", - compile.options.draft, { - default: false, - type: "boolean" - } - ); - response.check = this._parseAttribute("check", - compile.options.check, - {type: "string"}); - response.flags = this._parseAttribute("flags", - compile.options.flags, { - default: [], - type: "object" - } - ); - - // The syncType specifies whether the request contains all - // resources (full) or only those resources to be updated - // in-place (incremental). - response.syncType = this._parseAttribute("syncType", - compile.options.syncType, { - validValues: ["full", "incremental"], - type: "string" - } - ); - - // The syncState is an identifier passed in with the request - // which has the property that it changes when any resource is - // added, deleted, moved or renamed. - // - // on syncType full the syncState identifier is passed in and - // stored - // - // on syncType incremental the syncState identifier must match - // the stored value - response.syncState = this._parseAttribute("syncState", - compile.options.syncState, - {type: "string"}); - - if (response.timeout > RequestParser.MAX_TIMEOUT) { - response.timeout = RequestParser.MAX_TIMEOUT; - } - response.timeout = response.timeout * 1000; // milliseconds - - response.resources = ((() => { - const result = []; - for (resource of Array.from((compile.resources || []))) { result.push(this._parseResource(resource)); - } - return result; - })()); - - const rootResourcePath = this._parseAttribute("rootResourcePath", - compile.rootResourcePath, { - default: "main.tex", - type: "string" - } - ); - const originalRootResourcePath = rootResourcePath; - const sanitizedRootResourcePath = RequestParser._sanitizePath(rootResourcePath); - response.rootResourcePath = RequestParser._checkPath(sanitizedRootResourcePath); - - for (resource of Array.from(response.resources)) { - if (resource.path === originalRootResourcePath) { - resource.path = sanitizedRootResourcePath; - } - } - } catch (error1) { - const error = error1; - return callback(error); - } - - return callback(null, response); - }, - - _parseResource(resource) { - let modified; - if ((resource.path == null) || (typeof resource.path !== "string")) { - throw "all resources should have a path attribute"; - } - - if (resource.modified != null) { - modified = new Date(resource.modified); - if (isNaN(modified.getTime())) { - throw `resource modified date could not be understood: ${resource.modified}`; - } - } - - if ((resource.url == null) && (resource.content == null)) { - throw "all resources should have either a url or content attribute"; - } - if ((resource.content != null) && (typeof resource.content !== "string")) { - throw "content attribute should be a string"; - } - if ((resource.url != null) && (typeof resource.url !== "string")) { - throw "url attribute should be a string"; - } - - return { - path: resource.path, - modified, - url: resource.url, - content: resource.content - }; - }, - - _parseAttribute(name, attribute, options) { - if (attribute != null) { - if (options.validValues != null) { - if (options.validValues.indexOf(attribute) === -1) { - throw `${name} attribute should be one of: ${options.validValues.join(", ")}`; - } - } - if (options.type != null) { - if (typeof attribute !== options.type) { - throw `${name} attribute should be a ${options.type}`; - } - } - } else { - if (options.default != null) { return options.default; } - } - return attribute; - }, - - _sanitizePath(path) { - // See http://php.net/manual/en/function.escapeshellcmd.php - return path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, ""); - }, - - _checkPath(path) { - // check that the request does not use a relative path - for (const dir of Array.from(path.split('/'))) { - if (dir === '..') { - throw "relative path in root resource"; - } - } - return path; - } -}); +let RequestParser +const settings = require('settings-sharelatex') + +module.exports = RequestParser = { + VALID_COMPILERS: ['pdflatex', 'latex', 'xelatex', 'lualatex'], + MAX_TIMEOUT: 600, + + parse(body, callback) { + let resource + if (callback == null) { + callback = function(error, data) {} + } + const response = {} + + if (body.compile == null) { + return callback('top level object should have a compile attribute') + } + + const { compile } = body + if (!compile.options) { + compile.options = {} + } + + try { + response.compiler = this._parseAttribute( + 'compiler', + compile.options.compiler, + { + validValues: this.VALID_COMPILERS, + default: 'pdflatex', + type: 'string' + } + ) + response.timeout = this._parseAttribute( + 'timeout', + compile.options.timeout, + { + default: RequestParser.MAX_TIMEOUT, + type: 'number' + } + ) + response.imageName = this._parseAttribute( + 'imageName', + compile.options.imageName, + { type: 'string' } + ) + response.draft = this._parseAttribute('draft', compile.options.draft, { + default: false, + type: 'boolean' + }) + response.check = this._parseAttribute('check', compile.options.check, { + type: 'string' + }) + response.flags = this._parseAttribute('flags', compile.options.flags, { + default: [], + type: 'object' + }) + + // The syncType specifies whether the request contains all + // resources (full) or only those resources to be updated + // in-place (incremental). + response.syncType = this._parseAttribute( + 'syncType', + compile.options.syncType, + { + validValues: ['full', 'incremental'], + type: 'string' + } + ) + + // The syncState is an identifier passed in with the request + // which has the property that it changes when any resource is + // added, deleted, moved or renamed. + // + // on syncType full the syncState identifier is passed in and + // stored + // + // on syncType incremental the syncState identifier must match + // the stored value + response.syncState = this._parseAttribute( + 'syncState', + compile.options.syncState, + { type: 'string' } + ) + + if (response.timeout > RequestParser.MAX_TIMEOUT) { + response.timeout = RequestParser.MAX_TIMEOUT + } + response.timeout = response.timeout * 1000 // milliseconds + + response.resources = (() => { + const result = [] + for (resource of Array.from(compile.resources || [])) { + result.push(this._parseResource(resource)) + } + return result + })() + + const rootResourcePath = this._parseAttribute( + 'rootResourcePath', + compile.rootResourcePath, + { + default: 'main.tex', + type: 'string' + } + ) + const originalRootResourcePath = rootResourcePath + const sanitizedRootResourcePath = RequestParser._sanitizePath( + rootResourcePath + ) + response.rootResourcePath = RequestParser._checkPath( + sanitizedRootResourcePath + ) + + for (resource of Array.from(response.resources)) { + if (resource.path === originalRootResourcePath) { + resource.path = sanitizedRootResourcePath + } + } + } catch (error1) { + const error = error1 + return callback(error) + } + + return callback(null, response) + }, + + _parseResource(resource) { + let modified + if (resource.path == null || typeof resource.path !== 'string') { + throw 'all resources should have a path attribute' + } + + if (resource.modified != null) { + modified = new Date(resource.modified) + if (isNaN(modified.getTime())) { + throw `resource modified date could not be understood: ${resource.modified}` + } + } + + if (resource.url == null && resource.content == null) { + throw 'all resources should have either a url or content attribute' + } + if (resource.content != null && typeof resource.content !== 'string') { + throw 'content attribute should be a string' + } + if (resource.url != null && typeof resource.url !== 'string') { + throw 'url attribute should be a string' + } + + return { + path: resource.path, + modified, + url: resource.url, + content: resource.content + } + }, + + _parseAttribute(name, attribute, options) { + if (attribute != null) { + if (options.validValues != null) { + if (options.validValues.indexOf(attribute) === -1) { + throw `${name} attribute should be one of: ${options.validValues.join( + ', ' + )}` + } + } + if (options.type != null) { + if (typeof attribute !== options.type) { + throw `${name} attribute should be a ${options.type}` + } + } + } else { + if (options.default != null) { + return options.default + } + } + return attribute + }, + + _sanitizePath(path) { + // See http://php.net/manual/en/function.escapeshellcmd.php + return path.replace( + /[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, + '' + ) + }, + + _checkPath(path) { + // check that the request does not use a relative path + for (const dir of Array.from(path.split('/'))) { + if (dir === '..') { + throw 'relative path in root resource' + } + } + return path + } +} diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 45cfdc61..5a5d811c 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -13,102 +13,142 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let ResourceStateManager; -const Path = require("path"); -const fs = require("fs"); -const logger = require("logger-sharelatex"); -const settings = require("settings-sharelatex"); -const Errors = require("./Errors"); -const SafeReader = require("./SafeReader"); +let ResourceStateManager +const Path = require('path') +const fs = require('fs') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const Errors = require('./Errors') +const SafeReader = require('./SafeReader') -module.exports = (ResourceStateManager = { +module.exports = ResourceStateManager = { + // The sync state is an identifier which must match for an + // incremental update to be allowed. + // + // The initial value is passed in and stored on a full + // compile, along with the list of resources.. + // + // Subsequent incremental compiles must come with the same value - if + // not they will be rejected with a 409 Conflict response. The + // previous list of resources is returned. + // + // An incremental compile can only update existing files with new + // content. The sync state identifier must change if any docs or + // files are moved, added, deleted or renamed. - // The sync state is an identifier which must match for an - // incremental update to be allowed. - // - // The initial value is passed in and stored on a full - // compile, along with the list of resources.. - // - // Subsequent incremental compiles must come with the same value - if - // not they will be rejected with a 409 Conflict response. The - // previous list of resources is returned. - // - // An incremental compile can only update existing files with new - // content. The sync state identifier must change if any docs or - // files are moved, added, deleted or renamed. + SYNC_STATE_FILE: '.project-sync-state', + SYNC_STATE_MAX_SIZE: 128 * 1024, - SYNC_STATE_FILE: ".project-sync-state", - SYNC_STATE_MAX_SIZE: 128*1024, + saveProjectState(state, resources, basePath, callback) { + if (callback == null) { + callback = function(error) {} + } + const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) + if (state == null) { + // remove the file if no state passed in + logger.log({ state, basePath }, 'clearing sync state') + return fs.unlink(stateFile, function(err) { + if (err != null && err.code !== 'ENOENT') { + return callback(err) + } else { + return callback() + } + }) + } else { + logger.log({ state, basePath }, 'writing sync state') + const resourceList = Array.from(resources).map(resource => resource.path) + return fs.writeFile( + stateFile, + [...Array.from(resourceList), `stateHash:${state}`].join('\n'), + callback + ) + } + }, - saveProjectState(state, resources, basePath, callback) { - if (callback == null) { callback = function(error) {}; } - const stateFile = Path.join(basePath, this.SYNC_STATE_FILE); - if ((state == null)) { // remove the file if no state passed in - logger.log({state, basePath}, "clearing sync state"); - return fs.unlink(stateFile, function(err) { - if ((err != null) && (err.code !== 'ENOENT')) { - return callback(err); - } else { - return callback(); - } - }); - } else { - logger.log({state, basePath}, "writing sync state"); - const resourceList = (Array.from(resources).map((resource) => resource.path)); - return fs.writeFile(stateFile, [...Array.from(resourceList), `stateHash:${state}`].join("\n"), callback); - } - }, + checkProjectStateMatches(state, basePath, callback) { + if (callback == null) { + callback = function(error, resources) {} + } + const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) + const size = this.SYNC_STATE_MAX_SIZE + return SafeReader.readFile(stateFile, size, 'utf8', function( + err, + result, + bytesRead + ) { + if (err != null) { + return callback(err) + } + if (bytesRead === size) { + logger.error( + { file: stateFile, size, bytesRead }, + 'project state file truncated' + ) + } + const array = + __guard__(result != null ? result.toString() : undefined, x => + x.split('\n') + ) || [] + const adjustedLength = Math.max(array.length, 1) + const resourceList = array.slice(0, adjustedLength - 1) + const oldState = array[adjustedLength - 1] + const newState = `stateHash:${state}` + logger.log( + { state, oldState, basePath, stateMatches: newState === oldState }, + 'checking sync state' + ) + if (newState !== oldState) { + return callback( + new Errors.FilesOutOfSyncError('invalid state for incremental update') + ) + } else { + const resources = Array.from(resourceList).map(path => ({ path })) + return callback(null, resources) + } + }) + }, - checkProjectStateMatches(state, basePath, callback) { - if (callback == null) { callback = function(error, resources) {}; } - const stateFile = Path.join(basePath, this.SYNC_STATE_FILE); - const size = this.SYNC_STATE_MAX_SIZE; - return SafeReader.readFile(stateFile, size, 'utf8', function(err, result, bytesRead) { - if (err != null) { return callback(err); } - if (bytesRead === size) { - logger.error({file:stateFile, size, bytesRead}, "project state file truncated"); - } - const array = __guard__(result != null ? result.toString() : undefined, x => x.split("\n")) || []; - const adjustedLength = Math.max(array.length, 1); - const resourceList = array.slice(0, adjustedLength - 1); - const oldState = array[adjustedLength - 1]; - const newState = `stateHash:${state}`; - logger.log({state, oldState, basePath, stateMatches: (newState === oldState)}, "checking sync state"); - if (newState !== oldState) { - return callback(new Errors.FilesOutOfSyncError("invalid state for incremental update")); - } else { - const resources = (Array.from(resourceList).map((path) => ({path}))); - return callback(null, resources); - } - }); - }, - - checkResourceFiles(resources, allFiles, basePath, callback) { - // check the paths are all relative to current directory - let file; - if (callback == null) { callback = function(error) {}; } - for (file of Array.from(resources || [])) { - for (const dir of Array.from(__guard__(file != null ? file.path : undefined, x => x.split('/')))) { - if (dir === '..') { - return callback(new Error("relative path in resource file list")); - } - } - } - // check if any of the input files are not present in list of files - const seenFile = {}; - for (file of Array.from(allFiles)) { - seenFile[file] = true; - } - const missingFiles = (Array.from(resources).filter((resource) => !seenFile[resource.path]).map((resource) => resource.path)); - if ((missingFiles != null ? missingFiles.length : undefined) > 0) { - logger.err({missingFiles, basePath, allFiles, resources}, "missing input files for project"); - return callback(new Errors.FilesOutOfSyncError("resource files missing in incremental update")); - } else { - return callback(); - } - } -}); + checkResourceFiles(resources, allFiles, basePath, callback) { + // check the paths are all relative to current directory + let file + if (callback == null) { + callback = function(error) {} + } + for (file of Array.from(resources || [])) { + for (const dir of Array.from( + __guard__(file != null ? file.path : undefined, x => x.split('/')) + )) { + if (dir === '..') { + return callback(new Error('relative path in resource file list')) + } + } + } + // check if any of the input files are not present in list of files + const seenFile = {} + for (file of Array.from(allFiles)) { + seenFile[file] = true + } + const missingFiles = Array.from(resources) + .filter(resource => !seenFile[resource.path]) + .map(resource => resource.path) + if ((missingFiles != null ? missingFiles.length : undefined) > 0) { + logger.err( + { missingFiles, basePath, allFiles, resources }, + 'missing input files for project' + ) + return callback( + new Errors.FilesOutOfSyncError( + 'resource files missing in incremental update' + ) + ) + } else { + return callback() + } + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index 028fc533..ba9706bc 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -14,202 +14,339 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let ResourceWriter; -const UrlCache = require("./UrlCache"); -const Path = require("path"); -const fs = require("fs"); -const async = require("async"); -const mkdirp = require("mkdirp"); -const OutputFileFinder = require("./OutputFileFinder"); -const ResourceStateManager = require("./ResourceStateManager"); -const Metrics = require("./Metrics"); -const logger = require("logger-sharelatex"); -const settings = require("settings-sharelatex"); +let ResourceWriter +const UrlCache = require('./UrlCache') +const Path = require('path') +const fs = require('fs') +const async = require('async') +const mkdirp = require('mkdirp') +const OutputFileFinder = require('./OutputFileFinder') +const ResourceStateManager = require('./ResourceStateManager') +const Metrics = require('./Metrics') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') -const parallelFileDownloads = settings.parallelFileDownloads || 1; +const parallelFileDownloads = settings.parallelFileDownloads || 1 -module.exports = (ResourceWriter = { +module.exports = ResourceWriter = { + syncResourcesToDisk(request, basePath, callback) { + if (callback == null) { + callback = function(error, resourceList) {} + } + if (request.syncType === 'incremental') { + logger.log( + { project_id: request.project_id, user_id: request.user_id }, + 'incremental sync' + ) + return ResourceStateManager.checkProjectStateMatches( + request.syncState, + basePath, + function(error, resourceList) { + if (error != null) { + return callback(error) + } + return ResourceWriter._removeExtraneousFiles( + resourceList, + basePath, + function(error, outputFiles, allFiles) { + if (error != null) { + return callback(error) + } + return ResourceStateManager.checkResourceFiles( + resourceList, + allFiles, + basePath, + function(error) { + if (error != null) { + return callback(error) + } + return ResourceWriter.saveIncrementalResourcesToDisk( + request.project_id, + request.resources, + basePath, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, resourceList) + } + ) + } + ) + } + ) + } + ) + } else { + logger.log( + { project_id: request.project_id, user_id: request.user_id }, + 'full sync' + ) + return this.saveAllResourcesToDisk( + request.project_id, + request.resources, + basePath, + function(error) { + if (error != null) { + return callback(error) + } + return ResourceStateManager.saveProjectState( + request.syncState, + request.resources, + basePath, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, request.resources) + } + ) + } + ) + } + }, - syncResourcesToDisk(request, basePath, callback) { - if (callback == null) { callback = function(error, resourceList) {}; } - if (request.syncType === "incremental") { - logger.log({project_id: request.project_id, user_id: request.user_id}, "incremental sync"); - return ResourceStateManager.checkProjectStateMatches(request.syncState, basePath, function(error, resourceList) { - if (error != null) { return callback(error); } - return ResourceWriter._removeExtraneousFiles(resourceList, basePath, function(error, outputFiles, allFiles) { - if (error != null) { return callback(error); } - return ResourceStateManager.checkResourceFiles(resourceList, allFiles, basePath, function(error) { - if (error != null) { return callback(error); } - return ResourceWriter.saveIncrementalResourcesToDisk(request.project_id, request.resources, basePath, function(error) { - if (error != null) { return callback(error); } - return callback(null, resourceList); - }); - }); - }); - }); - } else { - logger.log({project_id: request.project_id, user_id: request.user_id}, "full sync"); - return this.saveAllResourcesToDisk(request.project_id, request.resources, basePath, function(error) { - if (error != null) { return callback(error); } - return ResourceStateManager.saveProjectState(request.syncState, request.resources, basePath, function(error) { - if (error != null) { return callback(error); } - return callback(null, request.resources); - }); - }); - } - }, + saveIncrementalResourcesToDisk(project_id, resources, basePath, callback) { + if (callback == null) { + callback = function(error) {} + } + return this._createDirectory(basePath, error => { + if (error != null) { + return callback(error) + } + const jobs = Array.from(resources).map(resource => + (resource => { + return callback => + this._writeResourceToDisk(project_id, resource, basePath, callback) + })(resource) + ) + return async.parallelLimit(jobs, parallelFileDownloads, callback) + }) + }, - saveIncrementalResourcesToDisk(project_id, resources, basePath, callback) { - if (callback == null) { callback = function(error) {}; } - return this._createDirectory(basePath, error => { - if (error != null) { return callback(error); } - const jobs = Array.from(resources).map((resource) => - (resource => { - return callback => this._writeResourceToDisk(project_id, resource, basePath, callback); - })(resource)); - return async.parallelLimit(jobs, parallelFileDownloads, callback); - }); - }, + saveAllResourcesToDisk(project_id, resources, basePath, callback) { + if (callback == null) { + callback = function(error) {} + } + return this._createDirectory(basePath, error => { + if (error != null) { + return callback(error) + } + return this._removeExtraneousFiles(resources, basePath, error => { + if (error != null) { + return callback(error) + } + const jobs = Array.from(resources).map(resource => + (resource => { + return callback => + this._writeResourceToDisk( + project_id, + resource, + basePath, + callback + ) + })(resource) + ) + return async.parallelLimit(jobs, parallelFileDownloads, callback) + }) + }) + }, - saveAllResourcesToDisk(project_id, resources, basePath, callback) { - if (callback == null) { callback = function(error) {}; } - return this._createDirectory(basePath, error => { - if (error != null) { return callback(error); } - return this._removeExtraneousFiles(resources, basePath, error => { - if (error != null) { return callback(error); } - const jobs = Array.from(resources).map((resource) => - (resource => { - return callback => this._writeResourceToDisk(project_id, resource, basePath, callback); - })(resource)); - return async.parallelLimit(jobs, parallelFileDownloads, callback); - }); - }); - }, + _createDirectory(basePath, callback) { + if (callback == null) { + callback = function(error) {} + } + return fs.mkdir(basePath, function(err) { + if (err != null) { + if (err.code === 'EEXIST') { + return callback() + } else { + logger.log({ err, dir: basePath }, 'error creating directory') + return callback(err) + } + } else { + return callback() + } + }) + }, - _createDirectory(basePath, callback) { - if (callback == null) { callback = function(error) {}; } - return fs.mkdir(basePath, function(err) { - if (err != null) { - if (err.code === 'EEXIST') { - return callback(); - } else { - logger.log({err, dir:basePath}, "error creating directory"); - return callback(err); - } - } else { - return callback(); - } - }); - }, + _removeExtraneousFiles(resources, basePath, _callback) { + if (_callback == null) { + _callback = function(error, outputFiles, allFiles) {} + } + const timer = new Metrics.Timer('unlink-output-files') + const callback = function(error, ...result) { + timer.done() + return _callback(error, ...Array.from(result)) + } - _removeExtraneousFiles(resources, basePath, _callback) { - if (_callback == null) { _callback = function(error, outputFiles, allFiles) {}; } - const timer = new Metrics.Timer("unlink-output-files"); - const callback = function(error, ...result) { - timer.done(); - return _callback(error, ...Array.from(result)); - }; + return OutputFileFinder.findOutputFiles(resources, basePath, function( + error, + outputFiles, + allFiles + ) { + if (error != null) { + return callback(error) + } - return OutputFileFinder.findOutputFiles(resources, basePath, function(error, outputFiles, allFiles) { - if (error != null) { return callback(error); } + const jobs = [] + for (const file of Array.from(outputFiles || [])) { + ;(function(file) { + const { path } = file + let should_delete = true + if ( + path.match(/^output\./) || + path.match(/\.aux$/) || + path.match(/^cache\//) + ) { + // knitr cache + should_delete = false + } + if (path.match(/^output-.*/)) { + // Tikz cached figures (default case) + should_delete = false + } + if (path.match(/\.(pdf|dpth|md5)$/)) { + // Tikz cached figures (by extension) + should_delete = false + } + if ( + path.match(/\.(pygtex|pygstyle)$/) || + path.match(/(^|\/)_minted-[^\/]+\//) + ) { + // minted files/directory + should_delete = false + } + if ( + path.match(/\.md\.tex$/) || + path.match(/(^|\/)_markdown_[^\/]+\//) + ) { + // markdown files/directory + should_delete = false + } + if (path.match(/-eps-converted-to\.pdf$/)) { + // Epstopdf generated files + should_delete = false + } + if ( + path === 'output.pdf' || + path === 'output.dvi' || + path === 'output.log' || + path === 'output.xdv' + ) { + should_delete = true + } + if (path === 'output.tex') { + // created by TikzManager if present in output files + should_delete = true + } + if (should_delete) { + return jobs.push(callback => + ResourceWriter._deleteFileIfNotDirectory( + Path.join(basePath, path), + callback + ) + ) + } + })(file) + } - const jobs = []; - for (const file of Array.from(outputFiles || [])) { - (function(file) { - const { path } = file; - let should_delete = true; - if (path.match(/^output\./) || path.match(/\.aux$/) || path.match(/^cache\//)) { // knitr cache - should_delete = false; - } - if (path.match(/^output-.*/)) { // Tikz cached figures (default case) - should_delete = false; - } - if (path.match(/\.(pdf|dpth|md5)$/)) { // Tikz cached figures (by extension) - should_delete = false; - } - if (path.match(/\.(pygtex|pygstyle)$/) || path.match(/(^|\/)_minted-[^\/]+\//)) { // minted files/directory - should_delete = false; - } - if (path.match(/\.md\.tex$/) || path.match(/(^|\/)_markdown_[^\/]+\//)) { // markdown files/directory - should_delete = false; - } - if (path.match(/-eps-converted-to\.pdf$/)) { // Epstopdf generated files - should_delete = false; - } - if ((path === "output.pdf") || (path === "output.dvi") || (path === "output.log") || (path === "output.xdv")) { - should_delete = true; - } - if (path === "output.tex") { // created by TikzManager if present in output files - should_delete = true; - } - if (should_delete) { - return jobs.push(callback => ResourceWriter._deleteFileIfNotDirectory(Path.join(basePath, path), callback)); - } - })(file); - } + return async.series(jobs, function(error) { + if (error != null) { + return callback(error) + } + return callback(null, outputFiles, allFiles) + }) + }) + }, - return async.series(jobs, function(error) { - if (error != null) { return callback(error); } - return callback(null, outputFiles, allFiles); - }); - }); - }, + _deleteFileIfNotDirectory(path, callback) { + if (callback == null) { + callback = function(error) {} + } + return fs.stat(path, function(error, stat) { + if (error != null && error.code === 'ENOENT') { + return callback() + } else if (error != null) { + logger.err( + { err: error, path }, + 'error stating file in deleteFileIfNotDirectory' + ) + return callback(error) + } else if (stat.isFile()) { + return fs.unlink(path, function(error) { + if (error != null) { + logger.err( + { err: error, path }, + 'error removing file in deleteFileIfNotDirectory' + ) + return callback(error) + } else { + return callback() + } + }) + } else { + return callback() + } + }) + }, - _deleteFileIfNotDirectory(path, callback) { - if (callback == null) { callback = function(error) {}; } - return fs.stat(path, function(error, stat) { - if ((error != null) && (error.code === 'ENOENT')) { - return callback(); - } else if (error != null) { - logger.err({err: error, path}, "error stating file in deleteFileIfNotDirectory"); - return callback(error); - } else if (stat.isFile()) { - return fs.unlink(path, function(error) { - if (error != null) { - logger.err({err: error, path}, "error removing file in deleteFileIfNotDirectory"); - return callback(error); - } else { - return callback(); - } - }); - } else { - return callback(); - } - }); - }, + _writeResourceToDisk(project_id, resource, basePath, callback) { + if (callback == null) { + callback = function(error) {} + } + return ResourceWriter.checkPath(basePath, resource.path, function( + error, + path + ) { + if (error != null) { + return callback(error) + } + return mkdirp(Path.dirname(path), function(error) { + if (error != null) { + return callback(error) + } + // TODO: Don't overwrite file if it hasn't been modified + if (resource.url != null) { + return UrlCache.downloadUrlToFile( + project_id, + resource.url, + path, + resource.modified, + function(err) { + if (err != null) { + logger.err( + { + err, + project_id, + path, + resource_url: resource.url, + modified: resource.modified + }, + 'error downloading file for resources' + ) + } + return callback() + } + ) // try and continue compiling even if http resource can not be downloaded at this time + } else { + const process = require('process') + fs.writeFile(path, resource.content, callback) + try { + let result + return (result = fs.lstatSync(path)) + } catch (e) {} + } + }) + }) + }, - _writeResourceToDisk(project_id, resource, basePath, callback) { - if (callback == null) { callback = function(error) {}; } - return ResourceWriter.checkPath(basePath, resource.path, function(error, path) { - if (error != null) { return callback(error); } - return mkdirp(Path.dirname(path), function(error) { - if (error != null) { return callback(error); } - // TODO: Don't overwrite file if it hasn't been modified - if (resource.url != null) { - return UrlCache.downloadUrlToFile(project_id, resource.url, path, resource.modified, function(err){ - if (err != null) { - logger.err({err, project_id, path, resource_url:resource.url, modified:resource.modified}, "error downloading file for resources"); - } - return callback(); - }); // try and continue compiling even if http resource can not be downloaded at this time - } else { - const process = require("process"); - fs.writeFile(path, resource.content, callback); - try { - let result; - return result = fs.lstatSync(path); - } catch (e) {} - } - }); - }); - }, - - checkPath(basePath, resourcePath, callback) { - const path = Path.normalize(Path.join(basePath, resourcePath)); - if (path.slice(0, basePath.length + 1) !== (basePath + "/")) { - return callback(new Error("resource path is outside root directory")); - } else { - return callback(null, path); - } - } -}); + checkPath(basePath, resourcePath, callback) { + const path = Path.normalize(Path.join(basePath, resourcePath)) + if (path.slice(0, basePath.length + 1) !== basePath + '/') { + return callback(new Error('resource path is outside root directory')) + } else { + return callback(null, path) + } + } +} diff --git a/app/js/SafeReader.js b/app/js/SafeReader.js index 2fd599b3..d909e37e 100644 --- a/app/js/SafeReader.js +++ b/app/js/SafeReader.js @@ -12,36 +12,49 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let SafeReader; -const fs = require("fs"); -const logger = require("logger-sharelatex"); +let SafeReader +const fs = require('fs') +const logger = require('logger-sharelatex') -module.exports = (SafeReader = { +module.exports = SafeReader = { + // safely read up to size bytes from a file and return result as a + // string - // safely read up to size bytes from a file and return result as a - // string + readFile(file, size, encoding, callback) { + if (callback == null) { + callback = function(error, result) {} + } + return fs.open(file, 'r', function(err, fd) { + if (err != null && err.code === 'ENOENT') { + return callback() + } + if (err != null) { + return callback(err) + } - readFile(file, size, encoding, callback) { - if (callback == null) { callback = function(error, result) {}; } - return fs.open(file, 'r', function(err, fd) { - if ((err != null) && (err.code === 'ENOENT')) { return callback(); } - if (err != null) { return callback(err); } - - // safely return always closing the file - const callbackWithClose = (err, ...result) => - fs.close(fd, function(err1) { - if (err != null) { return callback(err); } - if (err1 != null) { return callback(err1); } - return callback(null, ...Array.from(result)); - }) - ; - - const buff = new Buffer(size, 0); // fill with zeros - return fs.read(fd, buff, 0, buff.length, 0, function(err, bytesRead, buffer) { - if (err != null) { return callbackWithClose(err); } - const result = buffer.toString(encoding, 0, bytesRead); - return callbackWithClose(null, result, bytesRead); - }); - }); - } -}); + // safely return always closing the file + const callbackWithClose = (err, ...result) => + fs.close(fd, function(err1) { + if (err != null) { + return callback(err) + } + if (err1 != null) { + return callback(err1) + } + return callback(null, ...Array.from(result)) + }) + const buff = new Buffer(size, 0) // fill with zeros + return fs.read(fd, buff, 0, buff.length, 0, function( + err, + bytesRead, + buffer + ) { + if (err != null) { + return callbackWithClose(err) + } + const result = buffer.toString(encoding, 0, bytesRead) + return callbackWithClose(null, result, bytesRead) + }) + }) + } +} diff --git a/app/js/StaticServerForbidSymlinks.js b/app/js/StaticServerForbidSymlinks.js index 8ac3e489..999ae206 100644 --- a/app/js/StaticServerForbidSymlinks.js +++ b/app/js/StaticServerForbidSymlinks.js @@ -14,59 +14,81 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let ForbidSymlinks; -const Path = require("path"); -const fs = require("fs"); -const Settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); -const url = require("url"); +let ForbidSymlinks +const Path = require('path') +const fs = require('fs') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const url = require('url') -module.exports = (ForbidSymlinks = function(staticFn, root, options) { - const expressStatic = staticFn(root, options); - const basePath = Path.resolve(root); - return function(req, res, next) { - let file, project_id, result; - const path = __guard__(url.parse(req.url), x => x.pathname); - // check that the path is of the form /project_id_or_name/path/to/file.log - if (result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/)) { - project_id = result[1]; - file = result[2]; - } else { - logger.warn({path}, "unrecognized file request"); - return res.sendStatus(404); - } - // check that the file does not use a relative path - for (const dir of Array.from(file.split('/'))) { - if (dir === '..') { - logger.warn({path}, "attempt to use a relative path"); - return res.sendStatus(404); - } - } - // check that the requested path is normalized - const requestedFsPath = `${basePath}/${project_id}/${file}`; - if (requestedFsPath !== Path.normalize(requestedFsPath)) { - logger.error({path: requestedFsPath}, "requestedFsPath is not normalized"); - return res.sendStatus(404); - } - // check that the requested path is not a symlink - return fs.realpath(requestedFsPath, function(err, realFsPath){ - if (err != null) { - if (err.code === 'ENOENT') { - return res.sendStatus(404); - } else { - logger.error({err, requestedFsPath, realFsPath, path: req.params[0], project_id: req.params.project_id}, "error checking file access"); - return res.sendStatus(500); - } - } else if (requestedFsPath !== realFsPath) { - logger.warn({requestedFsPath, realFsPath, path: req.params[0], project_id: req.params.project_id}, "trying to access a different file (symlink), aborting"); - return res.sendStatus(404); - } else { - return expressStatic(req, res, next); - } - }); - }; -}); +module.exports = ForbidSymlinks = function(staticFn, root, options) { + const expressStatic = staticFn(root, options) + const basePath = Path.resolve(root) + return function(req, res, next) { + let file, project_id, result + const path = __guard__(url.parse(req.url), x => x.pathname) + // check that the path is of the form /project_id_or_name/path/to/file.log + if ((result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/))) { + project_id = result[1] + file = result[2] + } else { + logger.warn({ path }, 'unrecognized file request') + return res.sendStatus(404) + } + // check that the file does not use a relative path + for (const dir of Array.from(file.split('/'))) { + if (dir === '..') { + logger.warn({ path }, 'attempt to use a relative path') + return res.sendStatus(404) + } + } + // check that the requested path is normalized + const requestedFsPath = `${basePath}/${project_id}/${file}` + if (requestedFsPath !== Path.normalize(requestedFsPath)) { + logger.error( + { path: requestedFsPath }, + 'requestedFsPath is not normalized' + ) + return res.sendStatus(404) + } + // check that the requested path is not a symlink + return fs.realpath(requestedFsPath, function(err, realFsPath) { + if (err != null) { + if (err.code === 'ENOENT') { + return res.sendStatus(404) + } else { + logger.error( + { + err, + requestedFsPath, + realFsPath, + path: req.params[0], + project_id: req.params.project_id + }, + 'error checking file access' + ) + return res.sendStatus(500) + } + } else if (requestedFsPath !== realFsPath) { + logger.warn( + { + requestedFsPath, + realFsPath, + path: req.params[0], + project_id: req.params.project_id + }, + 'trying to access a different file (symlink), aborting' + ) + return res.sendStatus(404) + } else { + return expressStatic(req, res, next) + } + }) + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/app/js/TikzManager.js b/app/js/TikzManager.js index 9fa4a936..3c578735 100644 --- a/app/js/TikzManager.js +++ b/app/js/TikzManager.js @@ -11,52 +11,84 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let TikzManager; -const fs = require("fs"); -const Path = require("path"); -const ResourceWriter = require("./ResourceWriter"); -const SafeReader = require("./SafeReader"); -const logger = require("logger-sharelatex"); +let TikzManager +const fs = require('fs') +const Path = require('path') +const ResourceWriter = require('./ResourceWriter') +const SafeReader = require('./SafeReader') +const logger = require('logger-sharelatex') // for \tikzexternalize or pstool to work the main file needs to match the // jobname. Since we set the -jobname to output, we have to create a // copy of the main file as 'output.tex'. -module.exports = (TikzManager = { +module.exports = TikzManager = { + checkMainFile(compileDir, mainFile, resources, callback) { + // if there's already an output.tex file, we don't want to touch it + if (callback == null) { + callback = function(error, needsMainFile) {} + } + for (const resource of Array.from(resources)) { + if (resource.path === 'output.tex') { + logger.log({ compileDir, mainFile }, 'output.tex already in resources') + return callback(null, false) + } + } + // if there's no output.tex, see if we are using tikz/pgf or pstool in the main file + return ResourceWriter.checkPath(compileDir, mainFile, function( + error, + path + ) { + if (error != null) { + return callback(error) + } + return SafeReader.readFile(path, 65536, 'utf8', function(error, content) { + if (error != null) { + return callback(error) + } + const usesTikzExternalize = + (content != null + ? content.indexOf('\\tikzexternalize') + : undefined) >= 0 + const usesPsTool = + (content != null ? content.indexOf('{pstool}') : undefined) >= 0 + logger.log( + { compileDir, mainFile, usesTikzExternalize, usesPsTool }, + 'checked for packages needing main file as output.tex' + ) + const needsMainFile = usesTikzExternalize || usesPsTool + return callback(null, needsMainFile) + }) + }) + }, - checkMainFile(compileDir, mainFile, resources, callback) { - // if there's already an output.tex file, we don't want to touch it - if (callback == null) { callback = function(error, needsMainFile) {}; } - for (const resource of Array.from(resources)) { - if (resource.path === "output.tex") { - logger.log({compileDir, mainFile}, "output.tex already in resources"); - return callback(null, false); - } - } - // if there's no output.tex, see if we are using tikz/pgf or pstool in the main file - return ResourceWriter.checkPath(compileDir, mainFile, function(error, path) { - if (error != null) { return callback(error); } - return SafeReader.readFile(path, 65536, "utf8", function(error, content) { - if (error != null) { return callback(error); } - const usesTikzExternalize = (content != null ? content.indexOf("\\tikzexternalize") : undefined) >= 0; - const usesPsTool = (content != null ? content.indexOf("{pstool}") : undefined) >= 0; - logger.log({compileDir, mainFile, usesTikzExternalize, usesPsTool}, "checked for packages needing main file as output.tex"); - const needsMainFile = (usesTikzExternalize || usesPsTool); - return callback(null, needsMainFile); - }); - }); - }, - - injectOutputFile(compileDir, mainFile, callback) { - if (callback == null) { callback = function(error) {}; } - return ResourceWriter.checkPath(compileDir, mainFile, function(error, path) { - if (error != null) { return callback(error); } - return fs.readFile(path, "utf8", function(error, content) { - if (error != null) { return callback(error); } - logger.log({compileDir, mainFile}, "copied file to output.tex as project uses packages which require it"); - // use wx flag to ensure that output file does not already exist - return fs.writeFile(Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback); - }); - }); - } -}); + injectOutputFile(compileDir, mainFile, callback) { + if (callback == null) { + callback = function(error) {} + } + return ResourceWriter.checkPath(compileDir, mainFile, function( + error, + path + ) { + if (error != null) { + return callback(error) + } + return fs.readFile(path, 'utf8', function(error, content) { + if (error != null) { + return callback(error) + } + logger.log( + { compileDir, mainFile }, + 'copied file to output.tex as project uses packages which require it' + ) + // use wx flag to ensure that output file does not already exist + return fs.writeFile( + Path.join(compileDir, 'output.tex'), + content, + { flag: 'wx' }, + callback + ) + }) + }) + } +} diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index ade815b2..babdf9cf 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -12,185 +12,267 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let UrlCache; -const db = require("./db"); -const dbQueue = require("./DbQueue"); -const UrlFetcher = require("./UrlFetcher"); -const Settings = require("settings-sharelatex"); -const crypto = require("crypto"); -const fs = require("fs"); -const logger = require("logger-sharelatex"); -const async = require("async"); +let UrlCache +const db = require('./db') +const dbQueue = require('./DbQueue') +const UrlFetcher = require('./UrlFetcher') +const Settings = require('settings-sharelatex') +const crypto = require('crypto') +const fs = require('fs') +const logger = require('logger-sharelatex') +const async = require('async') -module.exports = (UrlCache = { - downloadUrlToFile(project_id, url, destPath, lastModified, callback) { - if (callback == null) { callback = function(error) {}; } - return UrlCache._ensureUrlIsInCache(project_id, url, lastModified, (error, pathToCachedUrl) => { - if (error != null) { return callback(error); } - return UrlCache._copyFile(pathToCachedUrl, destPath, function(error) { - if (error != null) { - return UrlCache._clearUrlDetails(project_id, url, () => callback(error)); - } else { - return callback(error); - } - }); - }); - }, +module.exports = UrlCache = { + downloadUrlToFile(project_id, url, destPath, lastModified, callback) { + if (callback == null) { + callback = function(error) {} + } + return UrlCache._ensureUrlIsInCache( + project_id, + url, + lastModified, + (error, pathToCachedUrl) => { + if (error != null) { + return callback(error) + } + return UrlCache._copyFile(pathToCachedUrl, destPath, function(error) { + if (error != null) { + return UrlCache._clearUrlDetails(project_id, url, () => + callback(error) + ) + } else { + return callback(error) + } + }) + } + ) + }, - clearProject(project_id, callback) { - if (callback == null) { callback = function(error) {}; } - return UrlCache._findAllUrlsInProject(project_id, function(error, urls) { - logger.log({project_id, url_count: urls.length}, "clearing project URLs"); - if (error != null) { return callback(error); } - const jobs = (Array.from(urls || [])).map((url) => - (url => - callback => - UrlCache._clearUrlFromCache(project_id, url, function(error) { - if (error != null) { - logger.error({err: error, project_id, url}, "error clearing project URL"); - } - return callback(); - }) - - )(url)); - return async.series(jobs, callback); - }); - }, + clearProject(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return UrlCache._findAllUrlsInProject(project_id, function(error, urls) { + logger.log( + { project_id, url_count: urls.length }, + 'clearing project URLs' + ) + if (error != null) { + return callback(error) + } + const jobs = Array.from(urls || []).map(url => + (url => callback => + UrlCache._clearUrlFromCache(project_id, url, function(error) { + if (error != null) { + logger.error( + { err: error, project_id, url }, + 'error clearing project URL' + ) + } + return callback() + }))(url) + ) + return async.series(jobs, callback) + }) + }, - _ensureUrlIsInCache(project_id, url, lastModified, callback) { - if (callback == null) { callback = function(error, pathOnDisk) {}; } - if (lastModified != null) { - // MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds. - // So round down to seconds - lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000); - } - return UrlCache._doesUrlNeedDownloading(project_id, url, lastModified, (error, needsDownloading) => { - if (error != null) { return callback(error); } - if (needsDownloading) { - logger.log({url, lastModified}, "downloading URL"); - return UrlFetcher.pipeUrlToFile(url, UrlCache._cacheFilePathForUrl(project_id, url), error => { - if (error != null) { return callback(error); } - return UrlCache._updateOrCreateUrlDetails(project_id, url, lastModified, error => { - if (error != null) { return callback(error); } - return callback(null, UrlCache._cacheFilePathForUrl(project_id, url)); - }); - }); - } else { - logger.log({url, lastModified}, "URL is up to date in cache"); - return callback(null, UrlCache._cacheFilePathForUrl(project_id, url)); - } - }); - }, - - _doesUrlNeedDownloading(project_id, url, lastModified, callback) { - if (callback == null) { callback = function(error, needsDownloading) {}; } - if ((lastModified == null)) { - return callback(null, true); - } - return UrlCache._findUrlDetails(project_id, url, function(error, urlDetails) { - if (error != null) { return callback(error); } - if ((urlDetails == null) || (urlDetails.lastModified == null) || (urlDetails.lastModified.getTime() < lastModified.getTime())) { - return callback(null, true); - } else { - return callback(null, false); - } - }); - }, + _ensureUrlIsInCache(project_id, url, lastModified, callback) { + if (callback == null) { + callback = function(error, pathOnDisk) {} + } + if (lastModified != null) { + // MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds. + // So round down to seconds + lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000) + } + return UrlCache._doesUrlNeedDownloading( + project_id, + url, + lastModified, + (error, needsDownloading) => { + if (error != null) { + return callback(error) + } + if (needsDownloading) { + logger.log({ url, lastModified }, 'downloading URL') + return UrlFetcher.pipeUrlToFile( + url, + UrlCache._cacheFilePathForUrl(project_id, url), + error => { + if (error != null) { + return callback(error) + } + return UrlCache._updateOrCreateUrlDetails( + project_id, + url, + lastModified, + error => { + if (error != null) { + return callback(error) + } + return callback( + null, + UrlCache._cacheFilePathForUrl(project_id, url) + ) + } + ) + } + ) + } else { + logger.log({ url, lastModified }, 'URL is up to date in cache') + return callback(null, UrlCache._cacheFilePathForUrl(project_id, url)) + } + } + ) + }, - _cacheFileNameForUrl(project_id, url) { - return project_id + ":" + crypto.createHash("md5").update(url).digest("hex"); - }, + _doesUrlNeedDownloading(project_id, url, lastModified, callback) { + if (callback == null) { + callback = function(error, needsDownloading) {} + } + if (lastModified == null) { + return callback(null, true) + } + return UrlCache._findUrlDetails(project_id, url, function( + error, + urlDetails + ) { + if (error != null) { + return callback(error) + } + if ( + urlDetails == null || + urlDetails.lastModified == null || + urlDetails.lastModified.getTime() < lastModified.getTime() + ) { + return callback(null, true) + } else { + return callback(null, false) + } + }) + }, - _cacheFilePathForUrl(project_id, url) { - return `${Settings.path.clsiCacheDir}/${UrlCache._cacheFileNameForUrl(project_id, url)}`; - }, + _cacheFileNameForUrl(project_id, url) { + return ( + project_id + + ':' + + crypto + .createHash('md5') + .update(url) + .digest('hex') + ) + }, - _copyFile(from, to, _callback) { - if (_callback == null) { _callback = function(error) {}; } - const callbackOnce = function(error) { - if (error != null) { - logger.error({err: error, from, to}, "error copying file from cache"); - } - _callback(error); - return _callback = function() {}; - }; - const writeStream = fs.createWriteStream(to); - const readStream = fs.createReadStream(from); - writeStream.on("error", callbackOnce); - readStream.on("error", callbackOnce); - writeStream.on("close", callbackOnce); - return writeStream.on("open", () => readStream.pipe(writeStream)); - }, + _cacheFilePathForUrl(project_id, url) { + return `${Settings.path.clsiCacheDir}/${UrlCache._cacheFileNameForUrl( + project_id, + url + )}` + }, - _clearUrlFromCache(project_id, url, callback) { - if (callback == null) { callback = function(error) {}; } - return UrlCache._clearUrlDetails(project_id, url, function(error) { - if (error != null) { return callback(error); } - return UrlCache._deleteUrlCacheFromDisk(project_id, url, function(error) { - if (error != null) { return callback(error); } - return callback(null); - }); - }); - }, + _copyFile(from, to, _callback) { + if (_callback == null) { + _callback = function(error) {} + } + const callbackOnce = function(error) { + if (error != null) { + logger.error({ err: error, from, to }, 'error copying file from cache') + } + _callback(error) + return (_callback = function() {}) + } + const writeStream = fs.createWriteStream(to) + const readStream = fs.createReadStream(from) + writeStream.on('error', callbackOnce) + readStream.on('error', callbackOnce) + writeStream.on('close', callbackOnce) + return writeStream.on('open', () => readStream.pipe(writeStream)) + }, - _deleteUrlCacheFromDisk(project_id, url, callback) { - if (callback == null) { callback = function(error) {}; } - return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function(error) { - if ((error != null) && (error.code !== 'ENOENT')) { // no error if the file isn't present - return callback(error); - } else { - return callback(); - } - }); - }, + _clearUrlFromCache(project_id, url, callback) { + if (callback == null) { + callback = function(error) {} + } + return UrlCache._clearUrlDetails(project_id, url, function(error) { + if (error != null) { + return callback(error) + } + return UrlCache._deleteUrlCacheFromDisk(project_id, url, function(error) { + if (error != null) { + return callback(error) + } + return callback(null) + }) + }) + }, - _findUrlDetails(project_id, url, callback) { - if (callback == null) { callback = function(error, urlDetails) {}; } - const job = cb=> - db.UrlCache.find({where: { url, project_id }}) - .then(urlDetails => cb(null, urlDetails)) - .error(cb) - ; - return dbQueue.queue.push(job, callback); - }, + _deleteUrlCacheFromDisk(project_id, url, callback) { + if (callback == null) { + callback = function(error) {} + } + return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function( + error + ) { + if (error != null && error.code !== 'ENOENT') { + // no error if the file isn't present + return callback(error) + } else { + return callback() + } + }) + }, - _updateOrCreateUrlDetails(project_id, url, lastModified, callback) { - if (callback == null) { callback = function(error) {}; } - const job = cb=> - db.UrlCache.findOrCreate({where: {url, project_id}}) - .spread( - (urlDetails, created) => - urlDetails.updateAttributes({lastModified}) - .then(() => cb()) - .error(cb) - ) - .error(cb) - ; - return dbQueue.queue.push(job, callback); - }, + _findUrlDetails(project_id, url, callback) { + if (callback == null) { + callback = function(error, urlDetails) {} + } + const job = cb => + db.UrlCache.find({ where: { url, project_id } }) + .then(urlDetails => cb(null, urlDetails)) + .error(cb) + return dbQueue.queue.push(job, callback) + }, - _clearUrlDetails(project_id, url, callback) { - if (callback == null) { callback = function(error) {}; } - const job = cb=> - db.UrlCache.destroy({where: {url, project_id}}) - .then(() => cb(null)) - .error(cb) - ; - return dbQueue.queue.push(job, callback); - }, + _updateOrCreateUrlDetails(project_id, url, lastModified, callback) { + if (callback == null) { + callback = function(error) {} + } + const job = cb => + db.UrlCache.findOrCreate({ where: { url, project_id } }) + .spread((urlDetails, created) => + urlDetails + .updateAttributes({ lastModified }) + .then(() => cb()) + .error(cb) + ) + .error(cb) + return dbQueue.queue.push(job, callback) + }, + _clearUrlDetails(project_id, url, callback) { + if (callback == null) { + callback = function(error) {} + } + const job = cb => + db.UrlCache.destroy({ where: { url, project_id } }) + .then(() => cb(null)) + .error(cb) + return dbQueue.queue.push(job, callback) + }, - _findAllUrlsInProject(project_id, callback) { - if (callback == null) { callback = function(error, urls) {}; } - const job = cb=> - db.UrlCache.findAll({where: { project_id }}) - .then( - urlEntries => cb(null, urlEntries.map(entry => entry.url))) - .error(cb) - ; - return dbQueue.queue.push(job, callback); - } -}); - - - + _findAllUrlsInProject(project_id, callback) { + if (callback == null) { + callback = function(error, urls) {} + } + const job = cb => + db.UrlCache.findAll({ where: { project_id } }) + .then(urlEntries => + cb( + null, + urlEntries.map(entry => entry.url) + ) + ) + .error(cb) + return dbQueue.queue.push(job, callback) + } +} diff --git a/app/js/UrlFetcher.js b/app/js/UrlFetcher.js index fec397c1..19c681ca 100644 --- a/app/js/UrlFetcher.js +++ b/app/js/UrlFetcher.js @@ -12,85 +12,109 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let UrlFetcher; -const request = require("request").defaults({jar: false}); -const fs = require("fs"); -const logger = require("logger-sharelatex"); -const settings = require("settings-sharelatex"); -const URL = require('url'); +let UrlFetcher +const request = require('request').defaults({ jar: false }) +const fs = require('fs') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const URL = require('url') -const oneMinute = 60 * 1000; +const oneMinute = 60 * 1000 -module.exports = (UrlFetcher = { - pipeUrlToFile(url, filePath, _callback) { - if (_callback == null) { _callback = function(error) {}; } - const callbackOnce = function(error) { - if (timeoutHandler != null) { clearTimeout(timeoutHandler); } - _callback(error); - return _callback = function() {}; - }; +module.exports = UrlFetcher = { + pipeUrlToFile(url, filePath, _callback) { + if (_callback == null) { + _callback = function(error) {} + } + const callbackOnce = function(error) { + if (timeoutHandler != null) { + clearTimeout(timeoutHandler) + } + _callback(error) + return (_callback = function() {}) + } - if (settings.filestoreDomainOveride != null) { - const p = URL.parse(url).path; - url = `${settings.filestoreDomainOveride}${p}`; - } - var timeoutHandler = setTimeout(function() { - timeoutHandler = null; - logger.error({url, filePath}, "Timed out downloading file to cache"); - return callbackOnce(new Error(`Timed out downloading file to cache ${url}`)); - } - // FIXME: maybe need to close fileStream here - , 3 * oneMinute); + if (settings.filestoreDomainOveride != null) { + const p = URL.parse(url).path + url = `${settings.filestoreDomainOveride}${p}` + } + var timeoutHandler = setTimeout( + function() { + timeoutHandler = null + logger.error({ url, filePath }, 'Timed out downloading file to cache') + return callbackOnce( + new Error(`Timed out downloading file to cache ${url}`) + ) + }, + // FIXME: maybe need to close fileStream here + 3 * oneMinute + ) - logger.log({url, filePath}, "started downloading url to cache"); - const urlStream = request.get({url, timeout: oneMinute}); - urlStream.pause(); // stop data flowing until we are ready + logger.log({ url, filePath }, 'started downloading url to cache') + const urlStream = request.get({ url, timeout: oneMinute }) + urlStream.pause() // stop data flowing until we are ready - // attach handlers before setting up pipes - urlStream.on("error", function(error) { - logger.error({err: error, url, filePath}, "error downloading url"); - return callbackOnce(error || new Error(`Something went wrong downloading the URL ${url}`)); - }); + // attach handlers before setting up pipes + urlStream.on('error', function(error) { + logger.error({ err: error, url, filePath }, 'error downloading url') + return callbackOnce( + error || new Error(`Something went wrong downloading the URL ${url}`) + ) + }) - urlStream.on("end", () => logger.log({url, filePath}, "finished downloading file into cache")); + urlStream.on('end', () => + logger.log({ url, filePath }, 'finished downloading file into cache') + ) - return urlStream.on("response", function(res) { - if ((res.statusCode >= 200) && (res.statusCode < 300)) { - const fileStream = fs.createWriteStream(filePath); + return urlStream.on('response', function(res) { + if (res.statusCode >= 200 && res.statusCode < 300) { + const fileStream = fs.createWriteStream(filePath) - // attach handlers before setting up pipes - fileStream.on('error', function(error) { - logger.error({err: error, url, filePath}, "error writing file into cache"); - return fs.unlink(filePath, function(err) { - if (err != null) { - logger.err({err, filePath}, "error deleting file from cache"); - } - return callbackOnce(error); - }); - }); + // attach handlers before setting up pipes + fileStream.on('error', function(error) { + logger.error( + { err: error, url, filePath }, + 'error writing file into cache' + ) + return fs.unlink(filePath, function(err) { + if (err != null) { + logger.err({ err, filePath }, 'error deleting file from cache') + } + return callbackOnce(error) + }) + }) - fileStream.on('finish', function() { - logger.log({url, filePath}, "finished writing file into cache"); - return callbackOnce(); - }); + fileStream.on('finish', function() { + logger.log({ url, filePath }, 'finished writing file into cache') + return callbackOnce() + }) - fileStream.on('pipe', () => logger.log({url, filePath}, "piping into filestream")); + fileStream.on('pipe', () => + logger.log({ url, filePath }, 'piping into filestream') + ) - urlStream.pipe(fileStream); - return urlStream.resume(); // now we are ready to handle the data - } else { - logger.error({statusCode: res.statusCode, url, filePath}, "unexpected status code downloading url to cache"); - // https://nodejs.org/api/http.html#http_class_http_clientrequest - // If you add a 'response' event handler, then you must consume - // the data from the response object, either by calling - // response.read() whenever there is a 'readable' event, or by - // adding a 'data' handler, or by calling the .resume() - // method. Until the data is consumed, the 'end' event will not - // fire. Also, until the data is read it will consume memory - // that can eventually lead to a 'process out of memory' error. - urlStream.resume(); // discard the data - return callbackOnce(new Error(`URL returned non-success status code: ${res.statusCode} ${url}`)); - } - }); - } -}); + urlStream.pipe(fileStream) + return urlStream.resume() // now we are ready to handle the data + } else { + logger.error( + { statusCode: res.statusCode, url, filePath }, + 'unexpected status code downloading url to cache' + ) + // https://nodejs.org/api/http.html#http_class_http_clientrequest + // If you add a 'response' event handler, then you must consume + // the data from the response object, either by calling + // response.read() whenever there is a 'readable' event, or by + // adding a 'data' handler, or by calling the .resume() + // method. Until the data is consumed, the 'end' event will not + // fire. Also, until the data is read it will consume memory + // that can eventually lead to a 'process out of memory' error. + urlStream.resume() // discard the data + return callbackOnce( + new Error( + `URL returned non-success status code: ${res.statusCode} ${url}` + ) + ) + } + }) + } +} diff --git a/app/js/db.js b/app/js/db.js index c5dd9800..c749af25 100644 --- a/app/js/db.js +++ b/app/js/db.js @@ -8,57 +8,60 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Sequelize = require("sequelize"); -const Settings = require("settings-sharelatex"); -const _ = require("underscore"); -const logger = require("logger-sharelatex"); +const Sequelize = require('sequelize') +const Settings = require('settings-sharelatex') +const _ = require('underscore') +const logger = require('logger-sharelatex') -const options = _.extend({logging:false}, Settings.mysql.clsi); +const options = _.extend({ logging: false }, Settings.mysql.clsi) -logger.log({dbPath:Settings.mysql.clsi.storage}, "connecting to db"); +logger.log({ dbPath: Settings.mysql.clsi.storage }, 'connecting to db') const sequelize = new Sequelize( - Settings.mysql.clsi.database, - Settings.mysql.clsi.username, - Settings.mysql.clsi.password, - options -); + Settings.mysql.clsi.database, + Settings.mysql.clsi.username, + Settings.mysql.clsi.password, + options +) -if (Settings.mysql.clsi.dialect === "sqlite") { - logger.log("running PRAGMA journal_mode=WAL;"); - sequelize.query("PRAGMA journal_mode=WAL;"); - sequelize.query("PRAGMA synchronous=OFF;"); - sequelize.query("PRAGMA read_uncommitted = true;"); +if (Settings.mysql.clsi.dialect === 'sqlite') { + logger.log('running PRAGMA journal_mode=WAL;') + sequelize.query('PRAGMA journal_mode=WAL;') + sequelize.query('PRAGMA synchronous=OFF;') + sequelize.query('PRAGMA read_uncommitted = true;') } module.exports = { - UrlCache: sequelize.define("UrlCache", { - url: Sequelize.STRING, - project_id: Sequelize.STRING, - lastModified: Sequelize.DATE - }, { - indexes: [ - {fields: ['url', 'project_id']}, - {fields: ['project_id']} - ] - }), + UrlCache: sequelize.define( + 'UrlCache', + { + url: Sequelize.STRING, + project_id: Sequelize.STRING, + lastModified: Sequelize.DATE + }, + { + indexes: [{ fields: ['url', 'project_id'] }, { fields: ['project_id'] }] + } + ), - Project: sequelize.define("Project", { - project_id: {type: Sequelize.STRING, primaryKey: true}, - lastAccessed: Sequelize.DATE - }, { - indexes: [ - {fields: ['lastAccessed']} - ] - }), + Project: sequelize.define( + 'Project', + { + project_id: { type: Sequelize.STRING, primaryKey: true }, + lastAccessed: Sequelize.DATE + }, + { + indexes: [{ fields: ['lastAccessed'] }] + } + ), - op: Sequelize.Op, - - sync() { - logger.log({dbPath:Settings.mysql.clsi.storage}, "syncing db schema"); - return sequelize.sync() - .then(() => logger.log("db sync complete")).catch(err=> console.log(err, "error syncing")); - } -}; + op: Sequelize.Op, - + sync() { + logger.log({ dbPath: Settings.mysql.clsi.storage }, 'syncing db schema') + return sequelize + .sync() + .then(() => logger.log('db sync complete')) + .catch(err => console.log(err, 'error syncing')) + } +} From 18e6b4715d6235f0408efbfdd9072a7f51c1ed69 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:14:56 +0100 Subject: [PATCH 475/709] decaffeinate: Rename CompileControllerTests.coffee and 17 other files from .coffee to .js --- .../{CompileControllerTests.coffee => CompileControllerTests.js} | 0 .../coffee/{CompileManagerTests.coffee => CompileManagerTests.js} | 0 .../{ContentTypeMapperTests.coffee => ContentTypeMapperTests.js} | 0 .../{DockerLockManagerTests.coffee => DockerLockManagerTests.js} | 0 .../coffee/{DockerRunnerTests.coffee => DockerRunnerTests.js} | 0 .../{DraftModeManagerTests.coffee => DraftModeManagerTests.js} | 0 test/unit/coffee/{LatexRunnerTests.coffee => LatexRunnerTests.js} | 0 test/unit/coffee/{LockManagerTests.coffee => LockManagerTests.js} | 0 .../{OutputFileFinderTests.coffee => OutputFileFinderTests.js} | 0 ...utputFileOptimiserTests.coffee => OutputFileOptimiserTests.js} | 0 ...tenceManagerTests.coffee => ProjectPersistenceManagerTests.js} | 0 .../coffee/{RequestParserTests.coffee => RequestParserTests.js} | 0 ...ourceStateManagerTests.coffee => ResourceStateManagerTests.js} | 0 .../coffee/{ResourceWriterTests.coffee => ResourceWriterTests.js} | 0 ...bidSymlinksTests.coffee => StaticServerForbidSymlinksTests.js} | 0 test/unit/coffee/{TikzManager.coffee => TikzManager.js} | 0 test/unit/coffee/{UrlCacheTests.coffee => UrlCacheTests.js} | 0 test/unit/coffee/{UrlFetcherTests.coffee => UrlFetcherTests.js} | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename test/unit/coffee/{CompileControllerTests.coffee => CompileControllerTests.js} (100%) rename test/unit/coffee/{CompileManagerTests.coffee => CompileManagerTests.js} (100%) rename test/unit/coffee/{ContentTypeMapperTests.coffee => ContentTypeMapperTests.js} (100%) rename test/unit/coffee/{DockerLockManagerTests.coffee => DockerLockManagerTests.js} (100%) rename test/unit/coffee/{DockerRunnerTests.coffee => DockerRunnerTests.js} (100%) rename test/unit/coffee/{DraftModeManagerTests.coffee => DraftModeManagerTests.js} (100%) rename test/unit/coffee/{LatexRunnerTests.coffee => LatexRunnerTests.js} (100%) rename test/unit/coffee/{LockManagerTests.coffee => LockManagerTests.js} (100%) rename test/unit/coffee/{OutputFileFinderTests.coffee => OutputFileFinderTests.js} (100%) rename test/unit/coffee/{OutputFileOptimiserTests.coffee => OutputFileOptimiserTests.js} (100%) rename test/unit/coffee/{ProjectPersistenceManagerTests.coffee => ProjectPersistenceManagerTests.js} (100%) rename test/unit/coffee/{RequestParserTests.coffee => RequestParserTests.js} (100%) rename test/unit/coffee/{ResourceStateManagerTests.coffee => ResourceStateManagerTests.js} (100%) rename test/unit/coffee/{ResourceWriterTests.coffee => ResourceWriterTests.js} (100%) rename test/unit/coffee/{StaticServerForbidSymlinksTests.coffee => StaticServerForbidSymlinksTests.js} (100%) rename test/unit/coffee/{TikzManager.coffee => TikzManager.js} (100%) rename test/unit/coffee/{UrlCacheTests.coffee => UrlCacheTests.js} (100%) rename test/unit/coffee/{UrlFetcherTests.coffee => UrlFetcherTests.js} (100%) diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.js similarity index 100% rename from test/unit/coffee/CompileControllerTests.coffee rename to test/unit/coffee/CompileControllerTests.js diff --git a/test/unit/coffee/CompileManagerTests.coffee b/test/unit/coffee/CompileManagerTests.js similarity index 100% rename from test/unit/coffee/CompileManagerTests.coffee rename to test/unit/coffee/CompileManagerTests.js diff --git a/test/unit/coffee/ContentTypeMapperTests.coffee b/test/unit/coffee/ContentTypeMapperTests.js similarity index 100% rename from test/unit/coffee/ContentTypeMapperTests.coffee rename to test/unit/coffee/ContentTypeMapperTests.js diff --git a/test/unit/coffee/DockerLockManagerTests.coffee b/test/unit/coffee/DockerLockManagerTests.js similarity index 100% rename from test/unit/coffee/DockerLockManagerTests.coffee rename to test/unit/coffee/DockerLockManagerTests.js diff --git a/test/unit/coffee/DockerRunnerTests.coffee b/test/unit/coffee/DockerRunnerTests.js similarity index 100% rename from test/unit/coffee/DockerRunnerTests.coffee rename to test/unit/coffee/DockerRunnerTests.js diff --git a/test/unit/coffee/DraftModeManagerTests.coffee b/test/unit/coffee/DraftModeManagerTests.js similarity index 100% rename from test/unit/coffee/DraftModeManagerTests.coffee rename to test/unit/coffee/DraftModeManagerTests.js diff --git a/test/unit/coffee/LatexRunnerTests.coffee b/test/unit/coffee/LatexRunnerTests.js similarity index 100% rename from test/unit/coffee/LatexRunnerTests.coffee rename to test/unit/coffee/LatexRunnerTests.js diff --git a/test/unit/coffee/LockManagerTests.coffee b/test/unit/coffee/LockManagerTests.js similarity index 100% rename from test/unit/coffee/LockManagerTests.coffee rename to test/unit/coffee/LockManagerTests.js diff --git a/test/unit/coffee/OutputFileFinderTests.coffee b/test/unit/coffee/OutputFileFinderTests.js similarity index 100% rename from test/unit/coffee/OutputFileFinderTests.coffee rename to test/unit/coffee/OutputFileFinderTests.js diff --git a/test/unit/coffee/OutputFileOptimiserTests.coffee b/test/unit/coffee/OutputFileOptimiserTests.js similarity index 100% rename from test/unit/coffee/OutputFileOptimiserTests.coffee rename to test/unit/coffee/OutputFileOptimiserTests.js diff --git a/test/unit/coffee/ProjectPersistenceManagerTests.coffee b/test/unit/coffee/ProjectPersistenceManagerTests.js similarity index 100% rename from test/unit/coffee/ProjectPersistenceManagerTests.coffee rename to test/unit/coffee/ProjectPersistenceManagerTests.js diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.js similarity index 100% rename from test/unit/coffee/RequestParserTests.coffee rename to test/unit/coffee/RequestParserTests.js diff --git a/test/unit/coffee/ResourceStateManagerTests.coffee b/test/unit/coffee/ResourceStateManagerTests.js similarity index 100% rename from test/unit/coffee/ResourceStateManagerTests.coffee rename to test/unit/coffee/ResourceStateManagerTests.js diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.js similarity index 100% rename from test/unit/coffee/ResourceWriterTests.coffee rename to test/unit/coffee/ResourceWriterTests.js diff --git a/test/unit/coffee/StaticServerForbidSymlinksTests.coffee b/test/unit/coffee/StaticServerForbidSymlinksTests.js similarity index 100% rename from test/unit/coffee/StaticServerForbidSymlinksTests.coffee rename to test/unit/coffee/StaticServerForbidSymlinksTests.js diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.js similarity index 100% rename from test/unit/coffee/TikzManager.coffee rename to test/unit/coffee/TikzManager.js diff --git a/test/unit/coffee/UrlCacheTests.coffee b/test/unit/coffee/UrlCacheTests.js similarity index 100% rename from test/unit/coffee/UrlCacheTests.coffee rename to test/unit/coffee/UrlCacheTests.js diff --git a/test/unit/coffee/UrlFetcherTests.coffee b/test/unit/coffee/UrlFetcherTests.js similarity index 100% rename from test/unit/coffee/UrlFetcherTests.coffee rename to test/unit/coffee/UrlFetcherTests.js From 79a0891feeacbaaa8f63fb07d56a99f34569183c Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:15:08 +0100 Subject: [PATCH 476/709] decaffeinate: Convert CompileControllerTests.coffee and 17 other files to JS --- test/unit/coffee/CompileControllerTests.js | 458 ++++--- test/unit/coffee/CompileManagerTests.js | 728 ++++++----- test/unit/coffee/ContentTypeMapperTests.js | 100 +- test/unit/coffee/DockerLockManagerTests.js | 303 +++-- test/unit/coffee/DockerRunnerTests.js | 1101 ++++++++++------- test/unit/coffee/DraftModeManagerTests.js | 124 +- test/unit/coffee/LatexRunnerTests.js | 162 ++- test/unit/coffee/LockManagerTests.js | 120 +- test/unit/coffee/OutputFileFinderTests.js | 122 +- test/unit/coffee/OutputFileOptimiserTests.js | 242 ++-- .../coffee/ProjectPersistenceManagerTests.js | 138 ++- test/unit/coffee/RequestParserTests.js | 645 ++++++---- test/unit/coffee/ResourceStateManagerTests.js | 252 ++-- test/unit/coffee/ResourceWriterTests.js | 655 +++++----- .../coffee/StaticServerForbidSymlinksTests.js | 367 +++--- test/unit/coffee/TikzManager.js | 267 ++-- test/unit/coffee/UrlCacheTests.js | 444 ++++--- test/unit/coffee/UrlFetcherTests.js | 268 ++-- 18 files changed, 3693 insertions(+), 2803 deletions(-) diff --git a/test/unit/coffee/CompileControllerTests.js b/test/unit/coffee/CompileControllerTests.js index 034adfcd..1defed70 100644 --- a/test/unit/coffee/CompileControllerTests.js +++ b/test/unit/coffee/CompileControllerTests.js @@ -1,217 +1,269 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/CompileController' -tk = require("timekeeper") - -describe "CompileController", -> - beforeEach -> - @CompileController = SandboxedModule.require modulePath, requires: - "./CompileManager": @CompileManager = {} - "./RequestParser": @RequestParser = {} - "settings-sharelatex": @Settings = - apis: - clsi: +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/CompileController'); +const tk = require("timekeeper"); + +describe("CompileController", function() { + beforeEach(function() { + this.CompileController = SandboxedModule.require(modulePath, { requires: { + "./CompileManager": (this.CompileManager = {}), + "./RequestParser": (this.RequestParser = {}), + "settings-sharelatex": (this.Settings = { + apis: { + clsi: { url: "http://clsi.example.com" - "./ProjectPersistenceManager": @ProjectPersistenceManager = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err:sinon.stub(), warn: sinon.stub()} - @Settings.externalUrl = "http://www.example.com" - @req = {} - @res = {} - @next = sinon.stub() - - describe "compile", -> - beforeEach -> - @req.body = { + } + } + }), + "./ProjectPersistenceManager": (this.ProjectPersistenceManager = {}), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), err:sinon.stub(), warn: sinon.stub()}) + } + }); + this.Settings.externalUrl = "http://www.example.com"; + this.req = {}; + this.res = {}; + return this.next = sinon.stub(); + }); + + describe("compile", function() { + beforeEach(function() { + this.req.body = { compile: "mock-body" - } - @req.params = - project_id: @project_id = "project-id-123" - @request = { + }; + this.req.params = + {project_id: (this.project_id = "project-id-123")}; + this.request = { compile: "mock-parsed-request" - } - @request_with_project_id = - compile: @request.compile - project_id: @project_id - @output_files = [{ - path: "output.pdf" - type: "pdf" + }; + this.request_with_project_id = { + compile: this.request.compile, + project_id: this.project_id + }; + this.output_files = [{ + path: "output.pdf", + type: "pdf", build: 1234 }, { - path: "output.log" - type: "log" + path: "output.log", + type: "log", build: 1234 - }] - @RequestParser.parse = sinon.stub().callsArgWith(1, null, @request) - @ProjectPersistenceManager.markProjectAsJustAccessed = sinon.stub().callsArg(1) - @res.status = sinon.stub().returnsThis() - @res.send = sinon.stub() - - describe "successfully", -> - beforeEach -> - @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, @output_files) - @CompileController.compile @req, @res - - it "should parse the request", -> - @RequestParser.parse - .calledWith(@req.body) - .should.equal true - - it "should run the compile for the specified project", -> - @CompileManager.doCompileWithLock - .calledWith(@request_with_project_id) - .should.equal true - - it "should mark the project as accessed", -> - @ProjectPersistenceManager.markProjectAsJustAccessed - .calledWith(@project_id) - .should.equal true - - it "should return the JSON response", -> - @res.status.calledWith(200).should.equal true - @res.send - .calledWith( - compile: - status: "success" - error: null - outputFiles: @output_files.map (file) => - url: "#{@Settings.apis.clsi.url}/project/#{@project_id}/build/#{file.build}/output/#{file.path}" - path: file.path - type: file.type - build: file.build - ) - .should.equal true + }]; + this.RequestParser.parse = sinon.stub().callsArgWith(1, null, this.request); + this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon.stub().callsArg(1); + this.res.status = sinon.stub().returnsThis(); + return this.res.send = sinon.stub(); + }); + + describe("successfully", function() { + beforeEach(function() { + this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, this.output_files); + return this.CompileController.compile(this.req, this.res); + }); + + it("should parse the request", function() { + return this.RequestParser.parse + .calledWith(this.req.body) + .should.equal(true); + }); + + it("should run the compile for the specified project", function() { + return this.CompileManager.doCompileWithLock + .calledWith(this.request_with_project_id) + .should.equal(true); + }); + + it("should mark the project as accessed", function() { + return this.ProjectPersistenceManager.markProjectAsJustAccessed + .calledWith(this.project_id) + .should.equal(true); + }); + + return it("should return the JSON response", function() { + this.res.status.calledWith(200).should.equal(true); + return this.res.send + .calledWith({ + compile: { + status: "success", + error: null, + outputFiles: this.output_files.map(file => { + return { + url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, + path: file.path, + type: file.type, + build: file.build + }; + }) + } + }) + .should.equal(true); + }); + }); - describe "with an error", -> - beforeEach -> - @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, new Error(@message = "error message"), null) - @CompileController.compile @req, @res + describe("with an error", function() { + beforeEach(function() { + this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, new Error(this.message = "error message"), null); + return this.CompileController.compile(this.req, this.res); + }); - it "should return the JSON response with the error", -> - @res.status.calledWith(500).should.equal true - @res.send - .calledWith( - compile: - status: "error" - error: @message + return it("should return the JSON response with the error", function() { + this.res.status.calledWith(500).should.equal(true); + return this.res.send + .calledWith({ + compile: { + status: "error", + error: this.message, outputFiles: [] - ) - .should.equal true - - describe "when the request times out", -> - beforeEach -> - @error = new Error(@message = "container timed out") - @error.timedout = true - @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, @error, null) - @CompileController.compile @req, @res + } + }) + .should.equal(true); + }); + }); + + describe("when the request times out", function() { + beforeEach(function() { + this.error = new Error(this.message = "container timed out"); + this.error.timedout = true; + this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, this.error, null); + return this.CompileController.compile(this.req, this.res); + }); - it "should return the JSON response with the timeout status", -> - @res.status.calledWith(200).should.equal true - @res.send - .calledWith( - compile: - status: "timedout" - error: @message + return it("should return the JSON response with the timeout status", function() { + this.res.status.calledWith(200).should.equal(true); + return this.res.send + .calledWith({ + compile: { + status: "timedout", + error: this.message, outputFiles: [] - ) - .should.equal true + } + }) + .should.equal(true); + }); + }); - describe "when the request returns no output files", -> - beforeEach -> - @CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, []) - @CompileController.compile @req, @res + return describe("when the request returns no output files", function() { + beforeEach(function() { + this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, []); + return this.CompileController.compile(this.req, this.res); + }); - it "should return the JSON response with the failure status", -> - @res.status.calledWith(200).should.equal true - @res.send - .calledWith( - compile: - error: null - status: "failure" + return it("should return the JSON response with the failure status", function() { + this.res.status.calledWith(200).should.equal(true); + return this.res.send + .calledWith({ + compile: { + error: null, + status: "failure", outputFiles: [] - ) - .should.equal true - - describe "syncFromCode", -> - beforeEach -> - @file = "main.tex" - @line = 42 - @column = 5 - @project_id = "mock-project-id" - @req.params = - project_id: @project_id - @req.query = - file: @file - line: @line.toString() - column: @column.toString() - @res.json = sinon.stub() - - @CompileManager.syncFromCode = sinon.stub().callsArgWith(5, null, @pdfPositions = ["mock-positions"]) - @CompileController.syncFromCode @req, @res, @next - - it "should find the corresponding location in the PDF", -> - @CompileManager.syncFromCode - .calledWith(@project_id, undefined, @file, @line, @column) - .should.equal true - - it "should return the positions", -> - @res.json - .calledWith( - pdf: @pdfPositions - ) - .should.equal true - - describe "syncFromPdf", -> - beforeEach -> - @page = 5 - @h = 100.23 - @v = 45.67 - @project_id = "mock-project-id" - @req.params = - project_id: @project_id - @req.query = - page: @page.toString() - h: @h.toString() - v: @v.toString() - @res.json = sinon.stub() - - @CompileManager.syncFromPdf = sinon.stub().callsArgWith(5, null, @codePositions = ["mock-positions"]) - @CompileController.syncFromPdf @req, @res, @next - - it "should find the corresponding location in the code", -> - @CompileManager.syncFromPdf - .calledWith(@project_id, undefined, @page, @h, @v) - .should.equal true - - it "should return the positions", -> - @res.json - .calledWith( - code: @codePositions - ) - .should.equal true - - describe "wordcount", -> - beforeEach -> - @file = "main.tex" - @project_id = "mock-project-id" - @req.params = - project_id: @project_id - @req.query = - file: @file - image: @image = "example.com/image" - @res.json = sinon.stub() - - @CompileManager.wordcount = sinon.stub().callsArgWith(4, null, @texcount = ["mock-texcount"]) - @CompileController.wordcount @req, @res, @next - - it "should return the word count of a file", -> - @CompileManager.wordcount - .calledWith(@project_id, undefined, @file, @image) - .should.equal true - - it "should return the texcount info", -> - @res.json - .calledWith( - texcount: @texcount - ) - .should.equal true + } + }) + .should.equal(true); + }); + }); + }); + + describe("syncFromCode", function() { + beforeEach(function() { + this.file = "main.tex"; + this.line = 42; + this.column = 5; + this.project_id = "mock-project-id"; + this.req.params = + {project_id: this.project_id}; + this.req.query = { + file: this.file, + line: this.line.toString(), + column: this.column.toString() + }; + this.res.json = sinon.stub(); + + this.CompileManager.syncFromCode = sinon.stub().callsArgWith(5, null, (this.pdfPositions = ["mock-positions"])); + return this.CompileController.syncFromCode(this.req, this.res, this.next); + }); + + it("should find the corresponding location in the PDF", function() { + return this.CompileManager.syncFromCode + .calledWith(this.project_id, undefined, this.file, this.line, this.column) + .should.equal(true); + }); + + return it("should return the positions", function() { + return this.res.json + .calledWith({ + pdf: this.pdfPositions + }) + .should.equal(true); + }); + }); + + describe("syncFromPdf", function() { + beforeEach(function() { + this.page = 5; + this.h = 100.23; + this.v = 45.67; + this.project_id = "mock-project-id"; + this.req.params = + {project_id: this.project_id}; + this.req.query = { + page: this.page.toString(), + h: this.h.toString(), + v: this.v.toString() + }; + this.res.json = sinon.stub(); + + this.CompileManager.syncFromPdf = sinon.stub().callsArgWith(5, null, (this.codePositions = ["mock-positions"])); + return this.CompileController.syncFromPdf(this.req, this.res, this.next); + }); + + it("should find the corresponding location in the code", function() { + return this.CompileManager.syncFromPdf + .calledWith(this.project_id, undefined, this.page, this.h, this.v) + .should.equal(true); + }); + + return it("should return the positions", function() { + return this.res.json + .calledWith({ + code: this.codePositions + }) + .should.equal(true); + }); + }); + + return describe("wordcount", function() { + beforeEach(function() { + this.file = "main.tex"; + this.project_id = "mock-project-id"; + this.req.params = + {project_id: this.project_id}; + this.req.query = { + file: this.file, + image: (this.image = "example.com/image") + }; + this.res.json = sinon.stub(); + + this.CompileManager.wordcount = sinon.stub().callsArgWith(4, null, (this.texcount = ["mock-texcount"])); + return this.CompileController.wordcount(this.req, this.res, this.next); + }); + + it("should return the word count of a file", function() { + return this.CompileManager.wordcount + .calledWith(this.project_id, undefined, this.file, this.image) + .should.equal(true); + }); + + return it("should return the texcount info", function() { + return this.res.json + .calledWith({ + texcount: this.texcount + }) + .should.equal(true); + }); + }); +}); diff --git a/test/unit/coffee/CompileManagerTests.js b/test/unit/coffee/CompileManagerTests.js index c4b0f859..5675ac15 100644 --- a/test/unit/coffee/CompileManagerTests.js +++ b/test/unit/coffee/CompileManagerTests.js @@ -1,356 +1,426 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/CompileManager' -tk = require("timekeeper") -EventEmitter = require("events").EventEmitter -Path = require "path" - -describe "CompileManager", -> - beforeEach -> - @CompileManager = SandboxedModule.require modulePath, requires: - "./LatexRunner": @LatexRunner = {} - "./ResourceWriter": @ResourceWriter = {} - "./OutputFileFinder": @OutputFileFinder = {} - "./OutputCacheManager": @OutputCacheManager = {} - "settings-sharelatex": @Settings = - path: +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/CompileManager'); +const tk = require("timekeeper"); +const { EventEmitter } = require("events"); +const Path = require("path"); + +describe("CompileManager", function() { + beforeEach(function() { + this.CompileManager = SandboxedModule.require(modulePath, { requires: { + "./LatexRunner": (this.LatexRunner = {}), + "./ResourceWriter": (this.ResourceWriter = {}), + "./OutputFileFinder": (this.OutputFileFinder = {}), + "./OutputCacheManager": (this.OutputCacheManager = {}), + "settings-sharelatex": (this.Settings = { + path: { compilesDir: "/compiles/dir" - synctexBaseDir: -> "/compile" - clsi: - docker: + }, + synctexBaseDir() { return "/compile"; }, + clsi: { + docker: { image: "SOMEIMAGE" - - "logger-sharelatex": @logger = { log: sinon.stub() , info:->} - "child_process": @child_process = {} - "./CommandRunner": @CommandRunner = {} - "./DraftModeManager": @DraftModeManager = {} - "./TikzManager": @TikzManager = {} - "./LockManager": @LockManager = {} - "fs": @fs = {} - "fs-extra": @fse = { ensureDir: sinon.stub().callsArg(1) } - @callback = sinon.stub() - @project_id = "project-id-123" - @user_id = "1234" - describe "doCompileWithLock", -> - beforeEach -> - @request = - resources: @resources = "mock-resources" - project_id: @project_id - user_id: @user_id - @output_files = ["foo", "bar"] - @Settings.compileDir = "compiles" - @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" - @CompileManager.doCompile = sinon.stub().callsArgWith(1, null, @output_files) - @LockManager.runWithLock = (lockFile, runner, callback) -> - runner (err, result...) -> - callback(err, result...) - - describe "when the project is not locked", -> - beforeEach -> - @CompileManager.doCompileWithLock @request, @callback - - it "should ensure that the compile directory exists", -> - @fse.ensureDir.calledWith(@compileDir) - .should.equal true - - it "should call doCompile with the request", -> - @CompileManager.doCompile - .calledWith(@request) - .should.equal true - - it "should call the callback with the output files", -> - @callback.calledWithExactly(null, @output_files) - .should.equal true - - describe "when the project is locked", -> - beforeEach -> - @error = new Error("locked") - @LockManager.runWithLock = (lockFile, runner, callback) => - callback(@error) - @CompileManager.doCompileWithLock @request, @callback - - it "should ensure that the compile directory exists", -> - @fse.ensureDir.calledWith(@compileDir) - .should.equal true - - it "should not call doCompile with the request", -> - @CompileManager.doCompile - .called.should.equal false - - it "should call the callback with the error", -> - @callback.calledWithExactly(@error) - .should.equal true - - describe "doCompile", -> - beforeEach -> - @output_files = [{ - path: "output.log" + } + } + }), + + "logger-sharelatex": (this.logger = { log: sinon.stub() , info() {}}), + "child_process": (this.child_process = {}), + "./CommandRunner": (this.CommandRunner = {}), + "./DraftModeManager": (this.DraftModeManager = {}), + "./TikzManager": (this.TikzManager = {}), + "./LockManager": (this.LockManager = {}), + "fs": (this.fs = {}), + "fs-extra": (this.fse = { ensureDir: sinon.stub().callsArg(1) }) + } + }); + this.callback = sinon.stub(); + this.project_id = "project-id-123"; + return this.user_id = "1234"; + }); + describe("doCompileWithLock", function() { + beforeEach(function() { + this.request = { + resources: (this.resources = "mock-resources"), + project_id: this.project_id, + user_id: this.user_id + }; + this.output_files = ["foo", "bar"]; + this.Settings.compileDir = "compiles"; + this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; + this.CompileManager.doCompile = sinon.stub().callsArgWith(1, null, this.output_files); + return this.LockManager.runWithLock = (lockFile, runner, callback) => + runner((err, ...result) => callback(err, ...Array.from(result))) + ; + }); + + describe("when the project is not locked", function() { + beforeEach(function() { + return this.CompileManager.doCompileWithLock(this.request, this.callback); + }); + + it("should ensure that the compile directory exists", function() { + return this.fse.ensureDir.calledWith(this.compileDir) + .should.equal(true); + }); + + it("should call doCompile with the request", function() { + return this.CompileManager.doCompile + .calledWith(this.request) + .should.equal(true); + }); + + return it("should call the callback with the output files", function() { + return this.callback.calledWithExactly(null, this.output_files) + .should.equal(true); + }); + }); + + return describe("when the project is locked", function() { + beforeEach(function() { + this.error = new Error("locked"); + this.LockManager.runWithLock = (lockFile, runner, callback) => { + return callback(this.error); + }; + return this.CompileManager.doCompileWithLock(this.request, this.callback); + }); + + it("should ensure that the compile directory exists", function() { + return this.fse.ensureDir.calledWith(this.compileDir) + .should.equal(true); + }); + + it("should not call doCompile with the request", function() { + return this.CompileManager.doCompile + .called.should.equal(false); + }); + + return it("should call the callback with the error", function() { + return this.callback.calledWithExactly(this.error) + .should.equal(true); + }); + }); + }); + + describe("doCompile", function() { + beforeEach(function() { + this.output_files = [{ + path: "output.log", type: "log" }, { - path: "output.pdf" + path: "output.pdf", type: "pdf" - }] - @build_files = [{ - path: "output.log" - type: "log" + }]; + this.build_files = [{ + path: "output.log", + type: "log", build: 1234 }, { - path: "output.pdf" - type: "pdf" + path: "output.pdf", + type: "pdf", build: 1234 - }] - @request = - resources: @resources = "mock-resources" - rootResourcePath: @rootResourcePath = "main.tex" - project_id: @project_id - user_id: @user_id - compiler: @compiler = "pdflatex" - timeout: @timeout = 42000 - imageName: @image = "example.com/image" - flags: @flags = ["-file-line-error"] - @env = {} - @Settings.compileDir = "compiles" - @compileDir = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" - @ResourceWriter.syncResourcesToDisk = sinon.stub().callsArgWith(2, null, @resources) - @LatexRunner.runLatex = sinon.stub().callsArg(2) - @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) - @OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, @build_files) - @DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) - @TikzManager.checkMainFile = sinon.stub().callsArg(3, false) - - describe "normally", -> - beforeEach -> - @CompileManager.doCompile @request, @callback - - it "should write the resources to disk", -> - @ResourceWriter.syncResourcesToDisk - .calledWith(@request, @compileDir) - .should.equal true - - it "should run LaTeX", -> - @LatexRunner.runLatex - .calledWith("#{@project_id}-#{@user_id}", { - directory: @compileDir - mainFile: @rootResourcePath - compiler: @compiler - timeout: @timeout - image: @image - flags: @flags - environment: @env + }]; + this.request = { + resources: (this.resources = "mock-resources"), + rootResourcePath: (this.rootResourcePath = "main.tex"), + project_id: this.project_id, + user_id: this.user_id, + compiler: (this.compiler = "pdflatex"), + timeout: (this.timeout = 42000), + imageName: (this.image = "example.com/image"), + flags: (this.flags = ["-file-line-error"]) + }; + this.env = {}; + this.Settings.compileDir = "compiles"; + this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; + this.ResourceWriter.syncResourcesToDisk = sinon.stub().callsArgWith(2, null, this.resources); + this.LatexRunner.runLatex = sinon.stub().callsArg(2); + this.OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, this.output_files); + this.OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, this.build_files); + this.DraftModeManager.injectDraftMode = sinon.stub().callsArg(1); + return this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false); + }); + + describe("normally", function() { + beforeEach(function() { + return this.CompileManager.doCompile(this.request, this.callback); + }); + + it("should write the resources to disk", function() { + return this.ResourceWriter.syncResourcesToDisk + .calledWith(this.request, this.compileDir) + .should.equal(true); + }); + + it("should run LaTeX", function() { + return this.LatexRunner.runLatex + .calledWith(`${this.project_id}-${this.user_id}`, { + directory: this.compileDir, + mainFile: this.rootResourcePath, + compiler: this.compiler, + timeout: this.timeout, + image: this.image, + flags: this.flags, + environment: this.env }) - .should.equal true - - it "should find the output files", -> - @OutputFileFinder.findOutputFiles - .calledWith(@resources, @compileDir) - .should.equal true - - it "should return the output files", -> - @callback.calledWith(null, @build_files).should.equal true - - it "should not inject draft mode by default", -> - @DraftModeManager.injectDraftMode.called.should.equal false - - describe "with draft mode", -> - beforeEach -> - @request.draft = true - @CompileManager.doCompile @request, @callback - - it "should inject the draft mode header", -> - @DraftModeManager.injectDraftMode - .calledWith(@compileDir + "/" + @rootResourcePath) - .should.equal true - - describe "with a check option", -> - beforeEach -> - @request.check = "error" - @CompileManager.doCompile @request, @callback - - it "should run chktex", -> - @LatexRunner.runLatex - .calledWith("#{@project_id}-#{@user_id}", { - directory: @compileDir - mainFile: @rootResourcePath - compiler: @compiler - timeout: @timeout - image: @image - flags: @flags + .should.equal(true); + }); + + it("should find the output files", function() { + return this.OutputFileFinder.findOutputFiles + .calledWith(this.resources, this.compileDir) + .should.equal(true); + }); + + it("should return the output files", function() { + return this.callback.calledWith(null, this.build_files).should.equal(true); + }); + + return it("should not inject draft mode by default", function() { + return this.DraftModeManager.injectDraftMode.called.should.equal(false); + }); + }); + + describe("with draft mode", function() { + beforeEach(function() { + this.request.draft = true; + return this.CompileManager.doCompile(this.request, this.callback); + }); + + return it("should inject the draft mode header", function() { + return this.DraftModeManager.injectDraftMode + .calledWith(this.compileDir + "/" + this.rootResourcePath) + .should.equal(true); + }); + }); + + describe("with a check option", function() { + beforeEach(function() { + this.request.check = "error"; + return this.CompileManager.doCompile(this.request, this.callback); + }); + + return it("should run chktex", function() { + return this.LatexRunner.runLatex + .calledWith(`${this.project_id}-${this.user_id}`, { + directory: this.compileDir, + mainFile: this.rootResourcePath, + compiler: this.compiler, + timeout: this.timeout, + image: this.image, + flags: this.flags, environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1, 'CHKTEX_ULIMIT_OPTIONS': '-t 5 -v 64000'} }) - .should.equal true - - describe "with a knitr file and check options", -> - beforeEach -> - @request.rootResourcePath = "main.Rtex" - @request.check = "error" - @CompileManager.doCompile @request, @callback - - it "should not run chktex", -> - @LatexRunner.runLatex - .calledWith("#{@project_id}-#{@user_id}", { - directory: @compileDir - mainFile: "main.Rtex" - compiler: @compiler - timeout: @timeout - image: @image - flags: @flags - environment: @env + .should.equal(true); + }); + }); + + return describe("with a knitr file and check options", function() { + beforeEach(function() { + this.request.rootResourcePath = "main.Rtex"; + this.request.check = "error"; + return this.CompileManager.doCompile(this.request, this.callback); + }); + + return it("should not run chktex", function() { + return this.LatexRunner.runLatex + .calledWith(`${this.project_id}-${this.user_id}`, { + directory: this.compileDir, + mainFile: "main.Rtex", + compiler: this.compiler, + timeout: this.timeout, + image: this.image, + flags: this.flags, + environment: this.env }) - .should.equal true - - describe "clearProject", -> - describe "succesfully", -> - beforeEach -> - @Settings.compileDir = "compiles" - @fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory: ()->true}) - @proc = new EventEmitter() - @proc.stdout = new EventEmitter() - @proc.stderr = new EventEmitter() - @child_process.spawn = sinon.stub().returns(@proc) - @CompileManager.clearProject @project_id, @user_id, @callback - @proc.emit "close", 0 - - it "should remove the project directory", -> - @child_process.spawn - .calledWith("rm", ["-r", "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}"]) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "with a non-success status code", -> - beforeEach -> - @Settings.compileDir = "compiles" - @fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory: ()->true}) - @proc = new EventEmitter() - @proc.stdout = new EventEmitter() - @proc.stderr = new EventEmitter() - @child_process.spawn = sinon.stub().returns(@proc) - @CompileManager.clearProject @project_id, @user_id, @callback - @proc.stderr.emit "data", @error = "oops" - @proc.emit "close", 1 - - it "should remove the project directory", -> - @child_process.spawn - .calledWith("rm", ["-r", "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}"]) - .should.equal true - - it "should call the callback with an error from the stderr", -> - @callback + .should.equal(true); + }); + }); + }); + + describe("clearProject", function() { + describe("succesfully", function() { + beforeEach(function() { + this.Settings.compileDir = "compiles"; + this.fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory(){ return true; }}); + this.proc = new EventEmitter(); + this.proc.stdout = new EventEmitter(); + this.proc.stderr = new EventEmitter(); + this.child_process.spawn = sinon.stub().returns(this.proc); + this.CompileManager.clearProject(this.project_id, this.user_id, this.callback); + return this.proc.emit("close", 0); + }); + + it("should remove the project directory", function() { + return this.child_process.spawn + .calledWith("rm", ["-r", `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`]) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + return describe("with a non-success status code", function() { + beforeEach(function() { + this.Settings.compileDir = "compiles"; + this.fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory(){ return true; }}); + this.proc = new EventEmitter(); + this.proc.stdout = new EventEmitter(); + this.proc.stderr = new EventEmitter(); + this.child_process.spawn = sinon.stub().returns(this.proc); + this.CompileManager.clearProject(this.project_id, this.user_id, this.callback); + this.proc.stderr.emit("data", (this.error = "oops")); + return this.proc.emit("close", 1); + }); + + it("should remove the project directory", function() { + return this.child_process.spawn + .calledWith("rm", ["-r", `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`]) + .should.equal(true); + }); + + return it("should call the callback with an error from the stderr", function() { + this.callback .calledWith(new Error()) - .should.equal true - - @callback.args[0][0].message.should.equal "rm -r #{@Settings.path.compilesDir}/#{@project_id}-#{@user_id} failed: #{@error}" - - describe "syncing", -> - beforeEach -> - @page = 1 - @h = 42.23 - @v = 87.56 - @width = 100.01 - @height = 234.56 - @line = 5 - @column = 3 - @file_name = "main.tex" - @child_process.execFile = sinon.stub() - @Settings.path.synctexBaseDir = (project_id) => "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" - - describe "syncFromCode", -> - beforeEach -> - @fs.stat = sinon.stub().callsArgWith(1, null,{isFile: ()->true}) - @stdout = "NODE\t#{@page}\t#{@h}\t#{@v}\t#{@width}\t#{@height}\n" - @CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout}) - @CompileManager.syncFromCode @project_id, @user_id, @file_name, @line, @column, @callback - - it "should execute the synctex binary", -> - bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" - file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}" - @CommandRunner.run + .should.equal(true); + + return this.callback.args[0][0].message.should.equal(`rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} failed: ${this.error}`); + }); + }); + }); + + describe("syncing", function() { + beforeEach(function() { + this.page = 1; + this.h = 42.23; + this.v = 87.56; + this.width = 100.01; + this.height = 234.56; + this.line = 5; + this.column = 3; + this.file_name = "main.tex"; + this.child_process.execFile = sinon.stub(); + return this.Settings.path.synctexBaseDir = project_id => `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; + }); + + describe("syncFromCode", function() { + beforeEach(function() { + this.fs.stat = sinon.stub().callsArgWith(1, null,{isFile(){ return true; }}); + this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n`; + this.CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:this.stdout}); + return this.CompileManager.syncFromCode(this.project_id, this.user_id, this.file_name, this.line, this.column, this.callback); + }); + + it("should execute the synctex binary", function() { + const bin_path = Path.resolve(__dirname + "/../../../bin/synctex"); + const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`; + const file_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}`; + return this.CommandRunner.run .calledWith( - "#{@project_id}-#{@user_id}", - ['/opt/synctex', 'code', synctex_path, file_path, @line, @column], - "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}", - @Settings.clsi.docker.image, + `${this.project_id}-${this.user_id}`, + ['/opt/synctex', 'code', synctex_path, file_path, this.line, this.column], + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + this.Settings.clsi.docker.image, 60000, {} - ).should.equal true + ).should.equal(true); + }); - it "should call the callback with the parsed output", -> - @callback + return it("should call the callback with the parsed output", function() { + return this.callback .calledWith(null, [{ - page: @page - h: @h - v: @v - height: @height - width: @width + page: this.page, + h: this.h, + v: this.v, + height: this.height, + width: this.width }]) - .should.equal true - - describe "syncFromPdf", -> - beforeEach -> - @fs.stat = sinon.stub().callsArgWith(1, null,{isFile: ()->true}) - @stdout = "NODE\t#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}\t#{@line}\t#{@column}\n" - @CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout}) - @CompileManager.syncFromPdf @project_id, @user_id, @page, @h, @v, @callback - - it "should execute the synctex binary", -> - bin_path = Path.resolve(__dirname + "/../../../bin/synctex") - synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf" - @CommandRunner.run + .should.equal(true); + }); + }); + + return describe("syncFromPdf", function() { + beforeEach(function() { + this.fs.stat = sinon.stub().callsArgWith(1, null,{isFile(){ return true; }}); + this.stdout = `NODE\t${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}\t${this.line}\t${this.column}\n`; + this.CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:this.stdout}); + return this.CompileManager.syncFromPdf(this.project_id, this.user_id, this.page, this.h, this.v, this.callback); + }); + + it("should execute the synctex binary", function() { + const bin_path = Path.resolve(__dirname + "/../../../bin/synctex"); + const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`; + return this.CommandRunner.run .calledWith( - "#{@project_id}-#{@user_id}", - ['/opt/synctex', "pdf", synctex_path, @page, @h, @v], - "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}", - @Settings.clsi.docker.image, + `${this.project_id}-${this.user_id}`, + ['/opt/synctex', "pdf", synctex_path, this.page, this.h, this.v], + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + this.Settings.clsi.docker.image, 60000, - {}).should.equal true + {}).should.equal(true); + }); - it "should call the callback with the parsed output", -> - @callback + return it("should call the callback with the parsed output", function() { + return this.callback .calledWith(null, [{ - file: @file_name - line: @line - column: @column + file: this.file_name, + line: this.line, + column: this.column }]) - .should.equal true - - describe "wordcount", -> - beforeEach -> - @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 - @timeout = 60 * 1000 - @file_name = "main.tex" - @Settings.path.compilesDir = "/local/compile/directory" - @image = "example.com/image" - - @CompileManager.wordcount @project_id, @user_id, @file_name, @image, @callback - - it "should run the texcount command", -> - @directory = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}" - @file_path = "$COMPILE_DIR/#{@file_name}" - @command =[ "texcount", "-nocol", "-inc", @file_path, "-out=" + @file_path + ".wc"] - - @CommandRunner.run - .calledWith("#{@project_id}-#{@user_id}", @command, @directory, @image, @timeout, {}) - .should.equal true - - it "should call the callback with the parsed output", -> - @callback + .should.equal(true); + }); + }); + }); + + return describe("wordcount", function() { + beforeEach(function() { + this.CommandRunner.run = sinon.stub().callsArg(6); + this.fs.readFile = sinon.stub().callsArgWith(2, null, (this.stdout = "Encoding: ascii\nWords in text: 2")); + this.callback = sinon.stub(); + + this.project_id; + this.timeout = 60 * 1000; + this.file_name = "main.tex"; + this.Settings.path.compilesDir = "/local/compile/directory"; + this.image = "example.com/image"; + + return this.CompileManager.wordcount(this.project_id, this.user_id, this.file_name, this.image, this.callback); + }); + + it("should run the texcount command", function() { + this.directory = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; + this.file_path = `$COMPILE_DIR/${this.file_name}`; + this.command =[ "texcount", "-nocol", "-inc", this.file_path, `-out=${this.file_path}.wc`]; + + return this.CommandRunner.run + .calledWith(`${this.project_id}-${this.user_id}`, this.command, this.directory, this.image, this.timeout, {}) + .should.equal(true); + }); + + return it("should call the callback with the parsed output", function() { + return this.callback .calledWith(null, { - encode: "ascii" - textWords: 2 - headWords: 0 - outside: 0 - headers: 0 - elements: 0 - mathInline: 0 - mathDisplay: 0 - errors: 0 + encode: "ascii", + textWords: 2, + headWords: 0, + outside: 0, + headers: 0, + elements: 0, + mathInline: 0, + mathDisplay: 0, + errors: 0, messages: "" }) - .should.equal true + .should.equal(true); + }); + }); +}); diff --git a/test/unit/coffee/ContentTypeMapperTests.js b/test/unit/coffee/ContentTypeMapperTests.js index 2439120b..64a60914 100644 --- a/test/unit/coffee/ContentTypeMapperTests.js +++ b/test/unit/coffee/ContentTypeMapperTests.js @@ -1,55 +1,75 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/ContentTypeMapper' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/ContentTypeMapper'); -describe 'ContentTypeMapper', -> +describe('ContentTypeMapper', function() { - beforeEach -> - @ContentTypeMapper = SandboxedModule.require modulePath + beforeEach(function() { + return this.ContentTypeMapper = SandboxedModule.require(modulePath); + }); - describe 'map', -> + return describe('map', function() { - it 'should map .txt to text/plain', -> - content_type = @ContentTypeMapper.map('example.txt') - content_type.should.equal 'text/plain' + it('should map .txt to text/plain', function() { + const content_type = this.ContentTypeMapper.map('example.txt'); + return content_type.should.equal('text/plain'); + }); - it 'should map .csv to text/csv', -> - content_type = @ContentTypeMapper.map('example.csv') - content_type.should.equal 'text/csv' + it('should map .csv to text/csv', function() { + const content_type = this.ContentTypeMapper.map('example.csv'); + return content_type.should.equal('text/csv'); + }); - it 'should map .pdf to application/pdf', -> - content_type = @ContentTypeMapper.map('example.pdf') - content_type.should.equal 'application/pdf' + it('should map .pdf to application/pdf', function() { + const content_type = this.ContentTypeMapper.map('example.pdf'); + return content_type.should.equal('application/pdf'); + }); - it 'should fall back to octet-stream', -> - content_type = @ContentTypeMapper.map('example.unknown') - content_type.should.equal 'application/octet-stream' + it('should fall back to octet-stream', function() { + const content_type = this.ContentTypeMapper.map('example.unknown'); + return content_type.should.equal('application/octet-stream'); + }); - describe 'coercing web files to plain text', -> + describe('coercing web files to plain text', function() { - it 'should map .js to plain text', -> - content_type = @ContentTypeMapper.map('example.js') - content_type.should.equal 'text/plain' + it('should map .js to plain text', function() { + const content_type = this.ContentTypeMapper.map('example.js'); + return content_type.should.equal('text/plain'); + }); - it 'should map .html to plain text', -> - content_type = @ContentTypeMapper.map('example.html') - content_type.should.equal 'text/plain' + it('should map .html to plain text', function() { + const content_type = this.ContentTypeMapper.map('example.html'); + return content_type.should.equal('text/plain'); + }); - it 'should map .css to plain text', -> - content_type = @ContentTypeMapper.map('example.css') - content_type.should.equal 'text/plain' + return it('should map .css to plain text', function() { + const content_type = this.ContentTypeMapper.map('example.css'); + return content_type.should.equal('text/plain'); + }); + }); - describe 'image files', -> + return describe('image files', function() { - it 'should map .png to image/png', -> - content_type = @ContentTypeMapper.map('example.png') - content_type.should.equal 'image/png' + it('should map .png to image/png', function() { + const content_type = this.ContentTypeMapper.map('example.png'); + return content_type.should.equal('image/png'); + }); - it 'should map .jpeg to image/jpeg', -> - content_type = @ContentTypeMapper.map('example.jpeg') - content_type.should.equal 'image/jpeg' + it('should map .jpeg to image/jpeg', function() { + const content_type = this.ContentTypeMapper.map('example.jpeg'); + return content_type.should.equal('image/jpeg'); + }); - it 'should map .svg to text/plain to protect against XSS (SVG can execute JS)', -> - content_type = @ContentTypeMapper.map('example.svg') - content_type.should.equal 'text/plain' + return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function() { + const content_type = this.ContentTypeMapper.map('example.svg'); + return content_type.should.equal('text/plain'); + }); + }); + }); +}); diff --git a/test/unit/coffee/DockerLockManagerTests.js b/test/unit/coffee/DockerLockManagerTests.js index 6161bec3..5ef3ca2c 100644 --- a/test/unit/coffee/DockerLockManagerTests.js +++ b/test/unit/coffee/DockerLockManagerTests.js @@ -1,145 +1,188 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -require "coffee-script" -modulePath = require('path').join __dirname, '../../../app/coffee/DockerLockManager' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +require("coffee-script"); +const modulePath = require('path').join(__dirname, '../../../app/coffee/DockerLockManager'); -describe "LockManager", -> - beforeEach -> - @LockManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @Settings = - clsi: docker: {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } +describe("LockManager", function() { + beforeEach(function() { + return this.LockManager = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.Settings = + {clsi: {docker: {}}}), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }) + } + });}); - describe "runWithLock", -> - describe "with a single lock", -> - beforeEach (done) -> - @callback = sinon.stub() - @LockManager.runWithLock "lock-one", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world") - , 100 - , (err, args...) => - @callback(err,args...) - done() + return describe("runWithLock", function() { + describe("with a single lock", function() { + beforeEach(function(done) { + this.callback = sinon.stub(); + return this.LockManager.runWithLock("lock-one", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world") + , 100) + + , (err, ...args) => { + this.callback(err,...Array.from(args)); + return done(); + }); + }); - it "should call the callback", -> - @callback.calledWith(null,"hello","world").should.equal true + return it("should call the callback", function() { + return this.callback.calledWith(null,"hello","world").should.equal(true); + }); + }); - describe "with two locks", -> - beforeEach (done) -> - @callback1 = sinon.stub() - @callback2 = sinon.stub() - @LockManager.runWithLock "lock-one", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","one") - , 100 - , (err, args...) => - @callback1(err,args...) - @LockManager.runWithLock "lock-two", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","two") - , 200 - , (err, args...) => - @callback2(err,args...) - done() + describe("with two locks", function() { + beforeEach(function(done) { + this.callback1 = sinon.stub(); + this.callback2 = sinon.stub(); + this.LockManager.runWithLock("lock-one", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","one") + , 100) + + , (err, ...args) => { + return this.callback1(err,...Array.from(args)); + }); + return this.LockManager.runWithLock("lock-two", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","two") + , 200) + + , (err, ...args) => { + this.callback2(err,...Array.from(args)); + return done(); + }); + }); - it "should call the first callback", -> - @callback1.calledWith(null,"hello","world","one").should.equal true + it("should call the first callback", function() { + return this.callback1.calledWith(null,"hello","world","one").should.equal(true); + }); - it "should call the second callback", -> - @callback2.calledWith(null,"hello","world","two").should.equal true + return it("should call the second callback", function() { + return this.callback2.calledWith(null,"hello","world","two").should.equal(true); + }); + }); - describe "with lock contention", -> - describe "where the first lock is released quickly", -> - beforeEach (done) -> - @LockManager.MAX_LOCK_WAIT_TIME = 1000 - @LockManager.LOCK_TEST_INTERVAL = 100 - @callback1 = sinon.stub() - @callback2 = sinon.stub() - @LockManager.runWithLock "lock", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","one") - , 100 - , (err, args...) => - @callback1(err,args...) - @LockManager.runWithLock "lock", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","two") - , 200 - , (err, args...) => - @callback2(err,args...) - done() + return describe("with lock contention", function() { + describe("where the first lock is released quickly", function() { + beforeEach(function(done) { + this.LockManager.MAX_LOCK_WAIT_TIME = 1000; + this.LockManager.LOCK_TEST_INTERVAL = 100; + this.callback1 = sinon.stub(); + this.callback2 = sinon.stub(); + this.LockManager.runWithLock("lock", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","one") + , 100) + + , (err, ...args) => { + return this.callback1(err,...Array.from(args)); + }); + return this.LockManager.runWithLock("lock", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","two") + , 200) + + , (err, ...args) => { + this.callback2(err,...Array.from(args)); + return done(); + }); + }); - it "should call the first callback", -> - @callback1.calledWith(null,"hello","world","one").should.equal true + it("should call the first callback", function() { + return this.callback1.calledWith(null,"hello","world","one").should.equal(true); + }); - it "should call the second callback", -> - @callback2.calledWith(null,"hello","world","two").should.equal true + return it("should call the second callback", function() { + return this.callback2.calledWith(null,"hello","world","two").should.equal(true); + }); + }); - describe "where the first lock is held longer than the waiting time", -> - beforeEach (done) -> - @LockManager.MAX_LOCK_HOLD_TIME = 10000 - @LockManager.MAX_LOCK_WAIT_TIME = 1000 - @LockManager.LOCK_TEST_INTERVAL = 100 - @callback1 = sinon.stub() - @callback2 = sinon.stub() - doneOne = doneTwo = false - finish = (key) -> - doneOne = true if key is 1 - doneTwo = true if key is 2 - done() if doneOne and doneTwo - @LockManager.runWithLock "lock", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","one") - , 1100 - , (err, args...) => - @callback1(err,args...) - finish(1) - @LockManager.runWithLock "lock", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","two") - , 100 - , (err, args...) => - @callback2(err,args...) - finish(2) + describe("where the first lock is held longer than the waiting time", function() { + beforeEach(function(done) { + let doneTwo; + this.LockManager.MAX_LOCK_HOLD_TIME = 10000; + this.LockManager.MAX_LOCK_WAIT_TIME = 1000; + this.LockManager.LOCK_TEST_INTERVAL = 100; + this.callback1 = sinon.stub(); + this.callback2 = sinon.stub(); + let doneOne = (doneTwo = false); + const finish = function(key) { + if (key === 1) { doneOne = true; } + if (key === 2) { doneTwo = true; } + if (doneOne && doneTwo) { return done(); } + }; + this.LockManager.runWithLock("lock", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","one") + , 1100) + + , (err, ...args) => { + this.callback1(err,...Array.from(args)); + return finish(1); + }); + return this.LockManager.runWithLock("lock", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","two") + , 100) + + , (err, ...args) => { + this.callback2(err,...Array.from(args)); + return finish(2); + }); + }); - it "should call the first callback", -> - @callback1.calledWith(null,"hello","world","one").should.equal true + it("should call the first callback", function() { + return this.callback1.calledWith(null,"hello","world","one").should.equal(true); + }); - it "should call the second callback with an error", -> - error = sinon.match.instanceOf Error - @callback2.calledWith(error).should.equal true + return it("should call the second callback with an error", function() { + const error = sinon.match.instanceOf(Error); + return this.callback2.calledWith(error).should.equal(true); + }); + }); - describe "where the first lock is held longer than the max holding time", -> - beforeEach (done) -> - @LockManager.MAX_LOCK_HOLD_TIME = 1000 - @LockManager.MAX_LOCK_WAIT_TIME = 2000 - @LockManager.LOCK_TEST_INTERVAL = 100 - @callback1 = sinon.stub() - @callback2 = sinon.stub() - doneOne = doneTwo = false - finish = (key) -> - doneOne = true if key is 1 - doneTwo = true if key is 2 - done() if doneOne and doneTwo - @LockManager.runWithLock "lock", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","one") - , 1500 - , (err, args...) => - @callback1(err,args...) - finish(1) - @LockManager.runWithLock "lock", (releaseLock) -> - setTimeout () -> - releaseLock(null, "hello", "world","two") - , 100 - , (err, args...) => - @callback2(err,args...) - finish(2) + return describe("where the first lock is held longer than the max holding time", function() { + beforeEach(function(done) { + let doneTwo; + this.LockManager.MAX_LOCK_HOLD_TIME = 1000; + this.LockManager.MAX_LOCK_WAIT_TIME = 2000; + this.LockManager.LOCK_TEST_INTERVAL = 100; + this.callback1 = sinon.stub(); + this.callback2 = sinon.stub(); + let doneOne = (doneTwo = false); + const finish = function(key) { + if (key === 1) { doneOne = true; } + if (key === 2) { doneTwo = true; } + if (doneOne && doneTwo) { return done(); } + }; + this.LockManager.runWithLock("lock", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","one") + , 1500) + + , (err, ...args) => { + this.callback1(err,...Array.from(args)); + return finish(1); + }); + return this.LockManager.runWithLock("lock", releaseLock => + setTimeout(() => releaseLock(null, "hello", "world","two") + , 100) + + , (err, ...args) => { + this.callback2(err,...Array.from(args)); + return finish(2); + }); + }); - it "should call the first callback", -> - @callback1.calledWith(null,"hello","world","one").should.equal true + it("should call the first callback", function() { + return this.callback1.calledWith(null,"hello","world","one").should.equal(true); + }); - it "should call the second callback", -> - @callback2.calledWith(null,"hello","world","two").should.equal true + return it("should call the second callback", function() { + return this.callback2.calledWith(null,"hello","world","two").should.equal(true); + }); + }); + }); + }); +}); diff --git a/test/unit/coffee/DockerRunnerTests.js b/test/unit/coffee/DockerRunnerTests.js index 307ffde1..79ac5df1 100644 --- a/test/unit/coffee/DockerRunnerTests.js +++ b/test/unit/coffee/DockerRunnerTests.js @@ -1,509 +1,656 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -expect = require('chai').expect -require "coffee-script" -modulePath = require('path').join __dirname, '../../../app/coffee/DockerRunner' -Path = require "path" - -describe "DockerRunner", -> - beforeEach -> - @container = container = {} - @DockerRunner = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @Settings = - clsi: docker: {} +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const { expect } = require('chai'); +require("coffee-script"); +const modulePath = require('path').join(__dirname, '../../../app/coffee/DockerRunner'); +const Path = require("path"); + +describe("DockerRunner", function() { + beforeEach(function() { + let container, Docker, Timer; + this.container = (container = {}); + this.DockerRunner = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.Settings = { + clsi: { docker: {} + }, path: {} - "logger-sharelatex": @logger = { + }), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), info: sinon.stub(), warn: sinon.stub() + }), + "dockerode": (Docker = (function() { + Docker = class Docker { + static initClass() { + this.prototype.getContainer = sinon.stub().returns(container); + this.prototype.createContainer = sinon.stub().yields(null, container); + this.prototype.listContainers = sinon.stub(); + } + }; + Docker.initClass(); + return Docker; + })()), + "fs": (this.fs = { stat: sinon.stub().yields(null,{isDirectory(){ return true; }}) }), + "./Metrics": { + Timer: (Timer = class Timer { + done() {} + }) + }, + "./LockManager": { + runWithLock(key, runner, callback) { return runner(callback); } } - "dockerode": class Docker - getContainer: sinon.stub().returns(container) - createContainer: sinon.stub().yields(null, container) - listContainers: sinon.stub() - "fs": @fs = { stat: sinon.stub().yields(null,{isDirectory:()->true}) } - "./Metrics": - Timer: class Timer - done: () -> - "./LockManager": - runWithLock: (key, runner, callback) -> runner(callback) - @Docker = Docker - @getContainer = Docker::getContainer - @createContainer = Docker::createContainer - @listContainers = Docker::listContainers - - @directory = "/local/compile/directory" - @mainFile = "main-file.tex" - @compiler = "pdflatex" - @image = "example.com/sharelatex/image:2016.2" - @env = {} - @callback = sinon.stub() - @project_id = "project-id-123" - @volumes = - "/local/compile/directory": "/compile" - @Settings.clsi.docker.image = @defaultImage = "default-image" - @Settings.clsi.docker.env = PATH: "mock-path" - - describe "run", -> - beforeEach (done)-> - @DockerRunner._getContainerOptions = sinon.stub().returns(@options = {mockoptions: "foo"}) - @DockerRunner._fingerprintContainer = sinon.stub().returns(@fingerprint = "fingerprint") - - @name = "project-#{@project_id}-#{@fingerprint}" - - @command = ["mock", "command", "--outdir=$COMPILE_DIR"] - @command_with_dir = ["mock", "command", "--outdir=/compile"] - @timeout = 42000 - done() - - describe "successfully", -> - beforeEach (done)-> - @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") - @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, (err, output)=> - @callback(err, output) - done() - - it "should generate the options for the container", -> - @DockerRunner._getContainerOptions - .calledWith(@command_with_dir, @image, @volumes, @timeout) - .should.equal true - - it "should generate the fingerprint from the returned options", -> - @DockerRunner._fingerprintContainer - .calledWith(@options) - .should.equal true - - it "should do the run", -> - @DockerRunner._runAndWaitForContainer - .calledWith(@options, @volumes, @timeout) - .should.equal true - - it "should call the callback", -> - @callback.calledWith(null, @output).should.equal true - - describe 'when path.sandboxedCompilesHostDir is set', -> - - beforeEach -> - @Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles' - @directory = '/var/lib/sharelatex/data/compiles/xyz' - @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") - @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback - - it 'should re-write the bind directory', -> - volumes = @DockerRunner._runAndWaitForContainer.lastCall.args[1] - expect(volumes).to.deep.equal { + } + } + ); + this.Docker = Docker; + this.getContainer = Docker.prototype.getContainer; + this.createContainer = Docker.prototype.createContainer; + this.listContainers = Docker.prototype.listContainers; + + this.directory = "/local/compile/directory"; + this.mainFile = "main-file.tex"; + this.compiler = "pdflatex"; + this.image = "example.com/sharelatex/image:2016.2"; + this.env = {}; + this.callback = sinon.stub(); + this.project_id = "project-id-123"; + this.volumes = + {"/local/compile/directory": "/compile"}; + this.Settings.clsi.docker.image = (this.defaultImage = "default-image"); + return this.Settings.clsi.docker.env = {PATH: "mock-path"}; + }); + + describe("run", function() { + beforeEach(function(done){ + this.DockerRunner._getContainerOptions = sinon.stub().returns(this.options = {mockoptions: "foo"}); + this.DockerRunner._fingerprintContainer = sinon.stub().returns(this.fingerprint = "fingerprint"); + + this.name = `project-${this.project_id}-${this.fingerprint}`; + + this.command = ["mock", "command", "--outdir=$COMPILE_DIR"]; + this.command_with_dir = ["mock", "command", "--outdir=/compile"]; + this.timeout = 42000; + return done(); + }); + + describe("successfully", function() { + beforeEach(function(done){ + this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); + return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, (err, output)=> { + this.callback(err, output); + return done(); + }); + }); + + it("should generate the options for the container", function() { + return this.DockerRunner._getContainerOptions + .calledWith(this.command_with_dir, this.image, this.volumes, this.timeout) + .should.equal(true); + }); + + it("should generate the fingerprint from the returned options", function() { + return this.DockerRunner._fingerprintContainer + .calledWith(this.options) + .should.equal(true); + }); + + it("should do the run", function() { + return this.DockerRunner._runAndWaitForContainer + .calledWith(this.options, this.volumes, this.timeout) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.calledWith(null, this.output).should.equal(true); + }); + }); + + describe('when path.sandboxedCompilesHostDir is set', function() { + + beforeEach(function() { + this.Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles'; + this.directory = '/var/lib/sharelatex/data/compiles/xyz'; + this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); + return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback); + }); + + it('should re-write the bind directory', function() { + const volumes = this.DockerRunner._runAndWaitForContainer.lastCall.args[1]; + return expect(volumes).to.deep.equal({ '/some/host/dir/compiles/xyz': '/compile' - } - - it "should call the callback", -> - @callback.calledWith(null, @output).should.equal true - - describe "when the run throws an error", -> - beforeEach -> - firstTime = true - @output = "mock-output" - @DockerRunner._runAndWaitForContainer = (options, volumes, timeout, callback = (error, output)->) => - if firstTime - firstTime = false - callback new Error("HTTP code is 500 which indicates error: server error") - else - callback(null, @output) - sinon.spy @DockerRunner, "_runAndWaitForContainer" - @DockerRunner.destroyContainer = sinon.stub().callsArg(3) - @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback - - it "should do the run twice", -> - @DockerRunner._runAndWaitForContainer - .calledTwice.should.equal true - - it "should destroy the container in between", -> - @DockerRunner.destroyContainer - .calledWith(@name, null) - .should.equal true - - it "should call the callback", -> - @callback.calledWith(null, @output).should.equal true + }); + }); + + return it("should call the callback", function() { + return this.callback.calledWith(null, this.output).should.equal(true); + }); + }); + + describe("when the run throws an error", function() { + beforeEach(function() { + let firstTime = true; + this.output = "mock-output"; + this.DockerRunner._runAndWaitForContainer = (options, volumes, timeout, callback) => { + if (callback == null) { callback = function(error, output){}; } + if (firstTime) { + firstTime = false; + return callback(new Error("HTTP code is 500 which indicates error: server error")); + } else { + return callback(null, this.output); + } + }; + sinon.spy(this.DockerRunner, "_runAndWaitForContainer"); + this.DockerRunner.destroyContainer = sinon.stub().callsArg(3); + return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback); + }); + + it("should do the run twice", function() { + return this.DockerRunner._runAndWaitForContainer + .calledTwice.should.equal(true); + }); + + it("should destroy the container in between", function() { + return this.DockerRunner.destroyContainer + .calledWith(this.name, null) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.calledWith(null, this.output).should.equal(true); + }); + }); - describe "with no image", -> - beforeEach -> - @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") - @DockerRunner.run @project_id, @command, @directory, null, @timeout, @env, @callback - - it "should use the default image", -> - @DockerRunner._getContainerOptions - .calledWith(@command_with_dir, @defaultImage, @volumes, @timeout) - .should.equal true + describe("with no image", function() { + beforeEach(function() { + this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); + return this.DockerRunner.run(this.project_id, this.command, this.directory, null, this.timeout, this.env, this.callback); + }); + + return it("should use the default image", function() { + return this.DockerRunner._getContainerOptions + .calledWith(this.command_with_dir, this.defaultImage, this.volumes, this.timeout) + .should.equal(true); + }); + }); - describe "with image override", -> - beforeEach -> - @Settings.texliveImageNameOveride = "overrideimage.com/something" - @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output") - @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback - - it "should use the override and keep the tag", -> - image = @DockerRunner._getContainerOptions.args[0][1] - image.should.equal "overrideimage.com/something/image:2016.2" - - describe "_runAndWaitForContainer", -> - beforeEach -> - @options = {mockoptions: "foo", name: @name = "mock-name"} - @DockerRunner.startContainer = (options, volumes, attachStreamHandler, callback) => - attachStreamHandler(null, @output = "mock-output") - callback(null, @containerId = "container-id") - sinon.spy @DockerRunner, "startContainer" - @DockerRunner.waitForContainer = sinon.stub().callsArgWith(2, null, @exitCode = 42) - @DockerRunner._runAndWaitForContainer @options, @volumes, @timeout, @callback - - it "should create/start the container", -> - @DockerRunner.startContainer - .calledWith(@options, @volumes) - .should.equal true - - it "should wait for the container to finish", -> - @DockerRunner.waitForContainer - .calledWith(@name, @timeout) - .should.equal true - - it "should call the callback with the output", -> - @callback.calledWith(null, @output).should.equal true - - describe "startContainer", -> - beforeEach -> - @attachStreamHandler = sinon.stub() - @attachStreamHandler.cock = true - @options = {mockoptions: "foo", name: "mock-name"} - @container.inspect = sinon.stub().callsArgWith(0) - @DockerRunner.attachToContainer = (containerId, attachStreamHandler, cb)=> - attachStreamHandler() - cb() - sinon.spy @DockerRunner, "attachToContainer" - - - - describe "when the container exists", -> - beforeEach -> - @container.inspect = sinon.stub().callsArgWith(0) - @container.start = sinon.stub().yields() - - @DockerRunner.startContainer @options, @volumes, @callback, -> - - it "should start the container with the given name", -> - @getContainer - .calledWith(@options.name) - .should.equal true - @container.start + return describe("with image override", function() { + beforeEach(function() { + this.Settings.texliveImageNameOveride = "overrideimage.com/something"; + this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); + return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback); + }); + + return it("should use the override and keep the tag", function() { + const image = this.DockerRunner._getContainerOptions.args[0][1]; + return image.should.equal("overrideimage.com/something/image:2016.2"); + }); + }); + }); + + describe("_runAndWaitForContainer", function() { + beforeEach(function() { + this.options = {mockoptions: "foo", name: (this.name = "mock-name")}; + this.DockerRunner.startContainer = (options, volumes, attachStreamHandler, callback) => { + attachStreamHandler(null, (this.output = "mock-output")); + return callback(null, (this.containerId = "container-id")); + }; + sinon.spy(this.DockerRunner, "startContainer"); + this.DockerRunner.waitForContainer = sinon.stub().callsArgWith(2, null, (this.exitCode = 42)); + return this.DockerRunner._runAndWaitForContainer(this.options, this.volumes, this.timeout, this.callback); + }); + + it("should create/start the container", function() { + return this.DockerRunner.startContainer + .calledWith(this.options, this.volumes) + .should.equal(true); + }); + + it("should wait for the container to finish", function() { + return this.DockerRunner.waitForContainer + .calledWith(this.name, this.timeout) + .should.equal(true); + }); + + return it("should call the callback with the output", function() { + return this.callback.calledWith(null, this.output).should.equal(true); + }); + }); + + describe("startContainer", function() { + beforeEach(function() { + this.attachStreamHandler = sinon.stub(); + this.attachStreamHandler.cock = true; + this.options = {mockoptions: "foo", name: "mock-name"}; + this.container.inspect = sinon.stub().callsArgWith(0); + this.DockerRunner.attachToContainer = (containerId, attachStreamHandler, cb)=> { + attachStreamHandler(); + return cb(); + }; + return sinon.spy(this.DockerRunner, "attachToContainer"); + }); + + + + describe("when the container exists", function() { + beforeEach(function() { + this.container.inspect = sinon.stub().callsArgWith(0); + this.container.start = sinon.stub().yields(); + + return this.DockerRunner.startContainer(this.options, this.volumes, this.callback, function() {}); + }); + + it("should start the container with the given name", function() { + this.getContainer + .calledWith(this.options.name) + .should.equal(true); + return this.container.start .called - .should.equal true + .should.equal(true); + }); - it "should not try to create the container", -> - @createContainer.called.should.equal false + it("should not try to create the container", function() { + return this.createContainer.called.should.equal(false); + }); - it "should attach to the container", -> - @DockerRunner.attachToContainer.called.should.equal true + it("should attach to the container", function() { + return this.DockerRunner.attachToContainer.called.should.equal(true); + }); - it "should call the callback", -> - @callback.called.should.equal true + it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); - it "should attach before the container starts", -> - sinon.assert.callOrder(@DockerRunner.attachToContainer, @container.start) + return it("should attach before the container starts", function() { + return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start); + }); + }); - describe "when the container does not exist", -> - beforeEach ()-> - exists = false - @container.start = sinon.stub().yields() - @container.inspect = sinon.stub().callsArgWith(0, {statusCode:404}) - @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback - - it "should create the container", -> - @createContainer - .calledWith(@options) - .should.equal true - - it "should call the callback and stream handler", -> - @attachStreamHandler.called.should.equal true - @callback.called.should.equal true + describe("when the container does not exist", function() { + beforeEach(function(){ + const exists = false; + this.container.start = sinon.stub().yields(); + this.container.inspect = sinon.stub().callsArgWith(0, {statusCode:404}); + return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); + }); + + it("should create the container", function() { + return this.createContainer + .calledWith(this.options) + .should.equal(true); + }); + + it("should call the callback and stream handler", function() { + this.attachStreamHandler.called.should.equal(true); + return this.callback.called.should.equal(true); + }); - it "should attach to the container", -> - @DockerRunner.attachToContainer.called.should.equal true - - it "should attach before the container starts", -> - sinon.assert.callOrder(@DockerRunner.attachToContainer, @container.start) - - - describe "when the container is already running", -> - beforeEach -> - error = new Error("HTTP code is 304 which indicates error: server error - start: Cannot start container #{@name}: The container MOCKID is already running.") - error.statusCode = 304 - @container.start = sinon.stub().yields(error) - @container.inspect = sinon.stub().callsArgWith(0) - @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback - - it "should not try to create the container", -> - @createContainer.called.should.equal false - - it "should call the callback and stream handler without an error", -> - @attachStreamHandler.called.should.equal true - @callback.called.should.equal true - - describe "when a volume does not exist", -> - beforeEach ()-> - @fs.stat = sinon.stub().yields(new Error("no such path")) - @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback - - it "should not try to create the container", -> - @createContainer.called.should.equal false - - it "should call the callback with an error", -> - @callback.calledWith(new Error()).should.equal true - - describe "when a volume exists but is not a directory", -> - beforeEach -> - @fs.stat = sinon.stub().yields(null, {isDirectory: () -> return false}) - @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback - - it "should not try to create the container", -> - @createContainer.called.should.equal false - - it "should call the callback with an error", -> - @callback.calledWith(new Error()).should.equal true - - describe "when a volume does not exist, but sibling-containers are used", -> - beforeEach -> - @fs.stat = sinon.stub().yields(new Error("no such path")) - @Settings.path.sandboxedCompilesHostDir = '/some/path' - @container.start = sinon.stub().yields() - @DockerRunner.startContainer @options, @volumes, @callback - - afterEach -> - delete @Settings.path.sandboxedCompilesHostDir - - it "should start the container with the given name", -> - @getContainer - .calledWith(@options.name) - .should.equal true - @container.start + it("should attach to the container", function() { + return this.DockerRunner.attachToContainer.called.should.equal(true); + }); + + return it("should attach before the container starts", function() { + return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start); + }); + }); + + + describe("when the container is already running", function() { + beforeEach(function() { + const error = new Error(`HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.`); + error.statusCode = 304; + this.container.start = sinon.stub().yields(error); + this.container.inspect = sinon.stub().callsArgWith(0); + return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); + }); + + it("should not try to create the container", function() { + return this.createContainer.called.should.equal(false); + }); + + return it("should call the callback and stream handler without an error", function() { + this.attachStreamHandler.called.should.equal(true); + return this.callback.called.should.equal(true); + }); + }); + + describe("when a volume does not exist", function() { + beforeEach(function(){ + this.fs.stat = sinon.stub().yields(new Error("no such path")); + return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); + }); + + it("should not try to create the container", function() { + return this.createContainer.called.should.equal(false); + }); + + return it("should call the callback with an error", function() { + return this.callback.calledWith(new Error()).should.equal(true); + }); + }); + + describe("when a volume exists but is not a directory", function() { + beforeEach(function() { + this.fs.stat = sinon.stub().yields(null, {isDirectory() { return false; }}); + return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); + }); + + it("should not try to create the container", function() { + return this.createContainer.called.should.equal(false); + }); + + return it("should call the callback with an error", function() { + return this.callback.calledWith(new Error()).should.equal(true); + }); + }); + + describe("when a volume does not exist, but sibling-containers are used", function() { + beforeEach(function() { + this.fs.stat = sinon.stub().yields(new Error("no such path")); + this.Settings.path.sandboxedCompilesHostDir = '/some/path'; + this.container.start = sinon.stub().yields(); + return this.DockerRunner.startContainer(this.options, this.volumes, this.callback); + }); + + afterEach(function() { + return delete this.Settings.path.sandboxedCompilesHostDir; + }); + + it("should start the container with the given name", function() { + this.getContainer + .calledWith(this.options.name) + .should.equal(true); + return this.container.start .called - .should.equal true - - it "should not try to create the container", -> - @createContainer.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - @callback.calledWith(new Error()).should.equal false - - describe "when the container tries to be created, but already has been (race condition)", -> - - describe "waitForContainer", -> - beforeEach -> - @containerId = "container-id" - @timeout = 5000 - @container.wait = sinon.stub().yields(null, StatusCode: @statusCode = 42) - @container.kill = sinon.stub().yields() + .should.equal(true); + }); + + it("should not try to create the container", function() { + return this.createContainer.called.should.equal(false); + }); + + return it("should call the callback", function() { + this.callback.called.should.equal(true); + return this.callback.calledWith(new Error()).should.equal(false); + }); + }); + + return describe("when the container tries to be created, but already has been (race condition)", function() {}); + }); + + describe("waitForContainer", function() { + beforeEach(function() { + this.containerId = "container-id"; + this.timeout = 5000; + this.container.wait = sinon.stub().yields(null, {StatusCode: (this.statusCode = 42)}); + return this.container.kill = sinon.stub().yields(); + }); - describe "when the container returns in time", -> - beforeEach -> - @DockerRunner.waitForContainer @containerId, @timeout, @callback - - it "should wait for the container", -> - @getContainer - .calledWith(@containerId) - .should.equal true - @container.wait + describe("when the container returns in time", function() { + beforeEach(function() { + return this.DockerRunner.waitForContainer(this.containerId, this.timeout, this.callback); + }); + + it("should wait for the container", function() { + this.getContainer + .calledWith(this.containerId) + .should.equal(true); + return this.container.wait .called - .should.equal true - - it "should call the callback with the exit", -> - @callback - .calledWith(null, @statusCode) - .should.equal true - - describe "when the container does not return before the timeout", -> - beforeEach (done) -> - @container.wait = (callback = (error, exitCode) ->) -> - setTimeout () -> - callback(null, StatusCode: 42) - , 100 - @timeout = 5 - @DockerRunner.waitForContainer @containerId, @timeout, (args...) => - @callback(args...) - done() - - it "should call kill on the container", -> - @getContainer - .calledWith(@containerId) - .should.equal true - @container.kill + .should.equal(true); + }); + + return it("should call the callback with the exit", function() { + return this.callback + .calledWith(null, this.statusCode) + .should.equal(true); + }); + }); + + return describe("when the container does not return before the timeout", function() { + beforeEach(function(done) { + this.container.wait = function(callback) { + if (callback == null) { callback = function(error, exitCode) {}; } + return setTimeout(() => callback(null, {StatusCode: 42}) + , 100); + }; + this.timeout = 5; + return this.DockerRunner.waitForContainer(this.containerId, this.timeout, (...args) => { + this.callback(...Array.from(args || [])); + return done(); + }); + }); + + it("should call kill on the container", function() { + this.getContainer + .calledWith(this.containerId) + .should.equal(true); + return this.container.kill .called - .should.equal true + .should.equal(true); + }); - it "should call the callback with an error", -> - error = new Error("container timed out") - error.timedout = true - @callback + return it("should call the callback with an error", function() { + const error = new Error("container timed out"); + error.timedout = true; + return this.callback .calledWith(error) - .should.equal true - - describe "destroyOldContainers", -> - beforeEach (done) -> - oneHourInSeconds = 60 * 60 - oneHourInMilliseconds = oneHourInSeconds * 1000 - nowInSeconds = Date.now()/1000 - @containers = [{ - Name: "/project-old-container-name" - Id: "old-container-id" + .should.equal(true); + }); + }); + }); + + describe("destroyOldContainers", function() { + beforeEach(function(done) { + const oneHourInSeconds = 60 * 60; + const oneHourInMilliseconds = oneHourInSeconds * 1000; + const nowInSeconds = Date.now()/1000; + this.containers = [{ + Name: "/project-old-container-name", + Id: "old-container-id", Created: nowInSeconds - oneHourInSeconds - 100 }, { - Name: "/project-new-container-name" - Id: "new-container-id" - Created: nowInSeconds - oneHourInSeconds + 100 + Name: "/project-new-container-name", + Id: "new-container-id", + Created: (nowInSeconds - oneHourInSeconds) + 100 }, { - Name: "/totally-not-a-project-container" - Id: "some-random-id" + Name: "/totally-not-a-project-container", + Id: "some-random-id", Created: nowInSeconds - (2 * oneHourInSeconds ) - }] - @DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds - @listContainers.callsArgWith(1, null, @containers) - @DockerRunner.destroyContainer = sinon.stub().callsArg(3) - @DockerRunner.destroyOldContainers (error) => - @callback(error) - done() - - it "should list all containers", -> - @listContainers - .calledWith(all: true) - .should.equal true - - it "should destroy old containers", -> - @DockerRunner.destroyContainer + }]; + this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds; + this.listContainers.callsArgWith(1, null, this.containers); + this.DockerRunner.destroyContainer = sinon.stub().callsArg(3); + return this.DockerRunner.destroyOldContainers(error => { + this.callback(error); + return done(); + }); + }); + + it("should list all containers", function() { + return this.listContainers + .calledWith({all: true}) + .should.equal(true); + }); + + it("should destroy old containers", function() { + this.DockerRunner.destroyContainer .callCount - .should.equal 1 - @DockerRunner.destroyContainer + .should.equal(1); + return this.DockerRunner.destroyContainer .calledWith("/project-old-container-name", "old-container-id") - .should.equal true + .should.equal(true); + }); - it "should not destroy new containers", -> - @DockerRunner.destroyContainer + it("should not destroy new containers", function() { + return this.DockerRunner.destroyContainer .calledWith("/project-new-container-name", "new-container-id") - .should.equal false + .should.equal(false); + }); - it "should not destroy non-project containers", -> - @DockerRunner.destroyContainer + it("should not destroy non-project containers", function() { + return this.DockerRunner.destroyContainer .calledWith("/totally-not-a-project-container", "some-random-id") - .should.equal false - - it "should callback the callback", -> - @callback.called.should.equal true - - - describe '_destroyContainer', -> - beforeEach -> - @containerId = 'some_id' - @fakeContainer = - remove: sinon.stub().callsArgWith(1, null) - @Docker::getContainer = sinon.stub().returns(@fakeContainer) - - it 'should get the container', (done) -> - @DockerRunner._destroyContainer @containerId, false, (err) => - @Docker::getContainer.callCount.should.equal 1 - @Docker::getContainer.calledWith(@containerId).should.equal true - done() - - it 'should try to force-destroy the container when shouldForce=true', (done) -> - @DockerRunner._destroyContainer @containerId, true, (err) => - @fakeContainer.remove.callCount.should.equal 1 - @fakeContainer.remove.calledWith({force: true}).should.equal true - done() - - it 'should not try to force-destroy the container when shouldForce=false', (done) -> - @DockerRunner._destroyContainer @containerId, false, (err) => - @fakeContainer.remove.callCount.should.equal 1 - @fakeContainer.remove.calledWith({force: false}).should.equal true - done() - - it 'should not produce an error', (done) -> - @DockerRunner._destroyContainer @containerId, false, (err) => - expect(err).to.equal null - done() - - describe 'when the container is already gone', -> - beforeEach -> - @fakeError = new Error('woops') - @fakeError.statusCode = 404 - @fakeContainer = - remove: sinon.stub().callsArgWith(1, @fakeError) - @Docker::getContainer = sinon.stub().returns(@fakeContainer) - - it 'should not produce an error', (done) -> - @DockerRunner._destroyContainer @containerId, false, (err) => - expect(err).to.equal null - done() - - describe 'when container.destroy produces an error', (done) -> - beforeEach -> - @fakeError = new Error('woops') - @fakeError.statusCode = 500 - @fakeContainer = - remove: sinon.stub().callsArgWith(1, @fakeError) - @Docker::getContainer = sinon.stub().returns(@fakeContainer) - - it 'should produce an error', (done) -> - @DockerRunner._destroyContainer @containerId, false, (err) => - expect(err).to.not.equal null - expect(err).to.equal @fakeError - done() - - - describe 'kill', -> - beforeEach -> - @containerId = 'some_id' - @fakeContainer = - kill: sinon.stub().callsArgWith(0, null) - @Docker::getContainer = sinon.stub().returns(@fakeContainer) - - it 'should get the container', (done) -> - @DockerRunner.kill @containerId, (err) => - @Docker::getContainer.callCount.should.equal 1 - @Docker::getContainer.calledWith(@containerId).should.equal true - done() - - it 'should try to force-destroy the container', (done) -> - @DockerRunner.kill @containerId, (err) => - @fakeContainer.kill.callCount.should.equal 1 - done() - - it 'should not produce an error', (done) -> - @DockerRunner.kill @containerId, (err) => - expect(err).to.equal undefined - done() - - describe 'when the container is not actually running', -> - beforeEach -> - @fakeError = new Error('woops') - @fakeError.statusCode = 500 - @fakeError.message = 'Cannot kill container <whatever> is not running' - @fakeContainer = - kill: sinon.stub().callsArgWith(0, @fakeError) - @Docker::getContainer = sinon.stub().returns(@fakeContainer) - - it 'should not produce an error', (done) -> - @DockerRunner.kill @containerId, (err) => - expect(err).to.equal undefined - done() - - describe 'when container.kill produces a legitimate error', (done) -> - beforeEach -> - @fakeError = new Error('woops') - @fakeError.statusCode = 500 - @fakeError.message = 'Totally legitimate reason to throw an error' - @fakeContainer = - kill: sinon.stub().callsArgWith(0, @fakeError) - @Docker::getContainer = sinon.stub().returns(@fakeContainer) - - it 'should produce an error', (done) -> - @DockerRunner.kill @containerId, (err) => - expect(err).to.not.equal undefined - expect(err).to.equal @fakeError - done() + .should.equal(false); + }); + + return it("should callback the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + + describe('_destroyContainer', function() { + beforeEach(function() { + this.containerId = 'some_id'; + this.fakeContainer = + {remove: sinon.stub().callsArgWith(1, null)}; + return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); + }); + + it('should get the container', function(done) { + return this.DockerRunner._destroyContainer(this.containerId, false, err => { + this.Docker.prototype.getContainer.callCount.should.equal(1); + this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true); + return done(); + }); + }); + + it('should try to force-destroy the container when shouldForce=true', function(done) { + return this.DockerRunner._destroyContainer(this.containerId, true, err => { + this.fakeContainer.remove.callCount.should.equal(1); + this.fakeContainer.remove.calledWith({force: true}).should.equal(true); + return done(); + }); + }); + + it('should not try to force-destroy the container when shouldForce=false', function(done) { + return this.DockerRunner._destroyContainer(this.containerId, false, err => { + this.fakeContainer.remove.callCount.should.equal(1); + this.fakeContainer.remove.calledWith({force: false}).should.equal(true); + return done(); + }); + }); + + it('should not produce an error', function(done) { + return this.DockerRunner._destroyContainer(this.containerId, false, err => { + expect(err).to.equal(null); + return done(); + }); + }); + + describe('when the container is already gone', function() { + beforeEach(function() { + this.fakeError = new Error('woops'); + this.fakeError.statusCode = 404; + this.fakeContainer = + {remove: sinon.stub().callsArgWith(1, this.fakeError)}; + return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); + }); + + return it('should not produce an error', function(done) { + return this.DockerRunner._destroyContainer(this.containerId, false, err => { + expect(err).to.equal(null); + return done(); + }); + }); + }); + + return describe('when container.destroy produces an error', function(done) { + beforeEach(function() { + this.fakeError = new Error('woops'); + this.fakeError.statusCode = 500; + this.fakeContainer = + {remove: sinon.stub().callsArgWith(1, this.fakeError)}; + return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); + }); + + return it('should produce an error', function(done) { + return this.DockerRunner._destroyContainer(this.containerId, false, err => { + expect(err).to.not.equal(null); + expect(err).to.equal(this.fakeError); + return done(); + }); + }); + }); + }); + + + return describe('kill', function() { + beforeEach(function() { + this.containerId = 'some_id'; + this.fakeContainer = + {kill: sinon.stub().callsArgWith(0, null)}; + return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); + }); + + it('should get the container', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + this.Docker.prototype.getContainer.callCount.should.equal(1); + this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true); + return done(); + }); + }); + + it('should try to force-destroy the container', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + this.fakeContainer.kill.callCount.should.equal(1); + return done(); + }); + }); + + it('should not produce an error', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + expect(err).to.equal(undefined); + return done(); + }); + }); + + describe('when the container is not actually running', function() { + beforeEach(function() { + this.fakeError = new Error('woops'); + this.fakeError.statusCode = 500; + this.fakeError.message = 'Cannot kill container <whatever> is not running'; + this.fakeContainer = + {kill: sinon.stub().callsArgWith(0, this.fakeError)}; + return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); + }); + + return it('should not produce an error', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + expect(err).to.equal(undefined); + return done(); + }); + }); + }); + + return describe('when container.kill produces a legitimate error', function(done) { + beforeEach(function() { + this.fakeError = new Error('woops'); + this.fakeError.statusCode = 500; + this.fakeError.message = 'Totally legitimate reason to throw an error'; + this.fakeContainer = + {kill: sinon.stub().callsArgWith(0, this.fakeError)}; + return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); + }); + + return it('should produce an error', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + expect(err).to.not.equal(undefined); + expect(err).to.equal(this.fakeError); + return done(); + }); + }); + }); + }); +}); diff --git a/test/unit/coffee/DraftModeManagerTests.js b/test/unit/coffee/DraftModeManagerTests.js index 549be293..ffea0508 100644 --- a/test/unit/coffee/DraftModeManagerTests.js +++ b/test/unit/coffee/DraftModeManagerTests.js @@ -1,61 +1,77 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/DraftModeManager' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/DraftModeManager'); -describe 'DraftModeManager', -> - beforeEach -> - @DraftModeManager = SandboxedModule.require modulePath, requires: - "fs": @fs = {} - "logger-sharelatex": @logger = {log: () ->} +describe('DraftModeManager', function() { + beforeEach(function() { + return this.DraftModeManager = SandboxedModule.require(modulePath, { requires: { + "fs": (this.fs = {}), + "logger-sharelatex": (this.logger = {log() {}}) + } + });}); - describe "_injectDraftOption", -> - it "should add draft option into documentclass with existing options", -> - @DraftModeManager - ._injectDraftOption(''' - \\documentclass[a4paper,foo=bar]{article} - ''') - .should.equal(''' - \\documentclass[draft,a4paper,foo=bar]{article} - ''') + describe("_injectDraftOption", function() { + it("should add draft option into documentclass with existing options", function() { + return this.DraftModeManager + ._injectDraftOption(`\ +\\documentclass[a4paper,foo=bar]{article}\ +`) + .should.equal(`\ +\\documentclass[draft,a4paper,foo=bar]{article}\ +`); + }); - it "should add draft option into documentclass with no options", -> - @DraftModeManager - ._injectDraftOption(''' - \\documentclass{article} - ''') - .should.equal(''' - \\documentclass[draft]{article} - ''') + return it("should add draft option into documentclass with no options", function() { + return this.DraftModeManager + ._injectDraftOption(`\ +\\documentclass{article}\ +`) + .should.equal(`\ +\\documentclass[draft]{article}\ +`); + }); + }); - describe "injectDraftMode", -> - beforeEach -> - @filename = "/mock/filename.tex" - @callback = sinon.stub() - content = ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' - @fs.readFile = sinon.stub().callsArgWith(2, null, content) - @fs.writeFile = sinon.stub().callsArg(2) - @DraftModeManager.injectDraftMode @filename, @callback + return describe("injectDraftMode", function() { + beforeEach(function() { + this.filename = "/mock/filename.tex"; + this.callback = sinon.stub(); + const content = `\ +\\documentclass{article} +\\begin{document} +Hello world +\\end{document}\ +`; + this.fs.readFile = sinon.stub().callsArgWith(2, null, content); + this.fs.writeFile = sinon.stub().callsArg(2); + return this.DraftModeManager.injectDraftMode(this.filename, this.callback); + }); - it "should read the file", -> - @fs.readFile - .calledWith(@filename, "utf8") - .should.equal true + it("should read the file", function() { + return this.fs.readFile + .calledWith(this.filename, "utf8") + .should.equal(true); + }); - it "should write the modified file", -> - @fs.writeFile - .calledWith(@filename, """ - \\documentclass[draft]{article} - \\begin{document} - Hello world - \\end{document} - """) - .should.equal true + it("should write the modified file", function() { + return this.fs.writeFile + .calledWith(this.filename, `\ +\\documentclass[draft]{article} +\\begin{document} +Hello world +\\end{document}\ +`) + .should.equal(true); + }); - it "should call the callback", -> - @callback.called.should.equal true + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); +}); diff --git a/test/unit/coffee/LatexRunnerTests.js b/test/unit/coffee/LatexRunnerTests.js index 77c6edbc..5cb4d066 100644 --- a/test/unit/coffee/LatexRunnerTests.js +++ b/test/unit/coffee/LatexRunnerTests.js @@ -1,79 +1,105 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/LatexRunner' -Path = require "path" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/LatexRunner'); +const Path = require("path"); -describe "LatexRunner", -> - beforeEach -> - @LatexRunner = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @Settings = - docker: +describe("LatexRunner", function() { + beforeEach(function() { + let Timer; + this.LatexRunner = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.Settings = { + docker: { socketPath: "/var/run/docker.sock" - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } - "./Metrics": - Timer: class Timer - done: () -> - "./CommandRunner": @CommandRunner = {} + } + }), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), + "./Metrics": { + Timer: (Timer = class Timer { + done() {} + }) + }, + "./CommandRunner": (this.CommandRunner = {}) + } + }); - @directory = "/local/compile/directory" - @mainFile = "main-file.tex" - @compiler = "pdflatex" - @image = "example.com/image" - @callback = sinon.stub() - @project_id = "project-id-123" - @env = {'foo': '123'} + this.directory = "/local/compile/directory"; + this.mainFile = "main-file.tex"; + this.compiler = "pdflatex"; + this.image = "example.com/image"; + this.callback = sinon.stub(); + this.project_id = "project-id-123"; + return this.env = {'foo': '123'};}); - describe "runLatex", -> - beforeEach -> - @CommandRunner.run = sinon.stub().callsArg(6) + return describe("runLatex", function() { + beforeEach(function() { + return this.CommandRunner.run = sinon.stub().callsArg(6); + }); - describe "normally", -> - beforeEach -> - @LatexRunner.runLatex @project_id, - directory: @directory - mainFile: @mainFile - compiler: @compiler - timeout: @timeout = 42000 - image: @image - environment: @env - @callback + describe("normally", function() { + beforeEach(function() { + return this.LatexRunner.runLatex(this.project_id, { + directory: this.directory, + mainFile: this.mainFile, + compiler: this.compiler, + timeout: (this.timeout = 42000), + image: this.image, + environment: this.env + }, + this.callback); + }); - it "should run the latex command", -> - @CommandRunner.run - .calledWith(@project_id, sinon.match.any, @directory, @image, @timeout, @env) - .should.equal true + return it("should run the latex command", function() { + return this.CommandRunner.run + .calledWith(this.project_id, sinon.match.any, this.directory, this.image, this.timeout, this.env) + .should.equal(true); + }); + }); - describe "with an .Rtex main file", -> - beforeEach -> - @LatexRunner.runLatex @project_id, - directory: @directory - mainFile: "main-file.Rtex" - compiler: @compiler - image: @image - timeout: @timeout = 42000 - @callback + describe("with an .Rtex main file", function() { + beforeEach(function() { + return this.LatexRunner.runLatex(this.project_id, { + directory: this.directory, + mainFile: "main-file.Rtex", + compiler: this.compiler, + image: this.image, + timeout: (this.timeout = 42000) + }, + this.callback); + }); - it "should run the latex command on the equivalent .tex file", -> - command = @CommandRunner.run.args[0][1] - mainFile = command.slice(-1)[0] - mainFile.should.equal "$COMPILE_DIR/main-file.tex" + return it("should run the latex command on the equivalent .tex file", function() { + const command = this.CommandRunner.run.args[0][1]; + const mainFile = command.slice(-1)[0]; + return mainFile.should.equal("$COMPILE_DIR/main-file.tex"); + }); + }); - describe "with a flags option", -> - beforeEach -> - @LatexRunner.runLatex @project_id, - directory: @directory - mainFile: @mainFile - compiler: @compiler - image: @image - timeout: @timeout = 42000 + return describe("with a flags option", function() { + beforeEach(function() { + return this.LatexRunner.runLatex(this.project_id, { + directory: this.directory, + mainFile: this.mainFile, + compiler: this.compiler, + image: this.image, + timeout: (this.timeout = 42000), flags: ["-file-line-error", "-halt-on-error"] - @callback + }, + this.callback); + }); - it "should include the flags in the command", -> - command = @CommandRunner.run.args[0][1] - flags = command.filter (arg) -> - (arg == "-file-line-error") || (arg == "-halt-on-error") - flags.length.should.equal 2 - flags[0].should.equal "-file-line-error" - flags[1].should.equal "-halt-on-error" + return it("should include the flags in the command", function() { + const command = this.CommandRunner.run.args[0][1]; + const flags = command.filter(arg => (arg === "-file-line-error") || (arg === "-halt-on-error")); + flags.length.should.equal(2); + flags[0].should.equal("-file-line-error"); + return flags[1].should.equal("-halt-on-error"); + }); + }); + }); +}); diff --git a/test/unit/coffee/LockManagerTests.js b/test/unit/coffee/LockManagerTests.js index 9dd1d46c..d716a443 100644 --- a/test/unit/coffee/LockManagerTests.js +++ b/test/unit/coffee/LockManagerTests.js @@ -1,57 +1,77 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/LockManager' -Path = require "path" -Errors = require "../../../app/js/Errors" - -describe "DockerLockManager", -> - beforeEach -> - @LockManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err:-> } - "fs": - lstat:sinon.stub().callsArgWith(1) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/LockManager'); +const Path = require("path"); +const Errors = require("../../../app/js/Errors"); + +describe("DockerLockManager", function() { + beforeEach(function() { + this.LockManager = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": {}, + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), err() {} }), + "fs": { + lstat:sinon.stub().callsArgWith(1), readdir: sinon.stub().callsArgWith(1) - "lockfile": @Lockfile = {} - @lockFile = "/local/compile/directory/.project-lock" - - describe "runWithLock", -> - beforeEach -> - @runner = sinon.stub().callsArgWith(0, null, "foo", "bar") - @callback = sinon.stub() - - describe "normally", -> - beforeEach -> - @Lockfile.lock = sinon.stub().callsArgWith(2, null) - @Lockfile.unlock = sinon.stub().callsArgWith(1, null) - @LockManager.runWithLock @lockFile, @runner, @callback - - it "should run the compile", -> - @runner + }, + "lockfile": (this.Lockfile = {}) + } + }); + return this.lockFile = "/local/compile/directory/.project-lock"; + }); + + return describe("runWithLock", function() { + beforeEach(function() { + this.runner = sinon.stub().callsArgWith(0, null, "foo", "bar"); + return this.callback = sinon.stub(); + }); + + describe("normally", function() { + beforeEach(function() { + this.Lockfile.lock = sinon.stub().callsArgWith(2, null); + this.Lockfile.unlock = sinon.stub().callsArgWith(1, null); + return this.LockManager.runWithLock(this.lockFile, this.runner, this.callback); + }); + + it("should run the compile", function() { + return this.runner .calledWith() - .should.equal true + .should.equal(true); + }); - it "should call the callback with the response from the compile", -> - @callback + return it("should call the callback with the response from the compile", function() { + return this.callback .calledWithExactly(null, "foo", "bar") - .should.equal true - - describe "when the project is locked", -> - beforeEach -> - @error = new Error() - @error.code = "EEXIST" - @Lockfile.lock = sinon.stub().callsArgWith(2,@error) - @Lockfile.unlock = sinon.stub().callsArgWith(1, null) - @LockManager.runWithLock @lockFile, @runner, @callback - - it "should not run the compile", -> - @runner + .should.equal(true); + }); + }); + + return describe("when the project is locked", function() { + beforeEach(function() { + this.error = new Error(); + this.error.code = "EEXIST"; + this.Lockfile.lock = sinon.stub().callsArgWith(2,this.error); + this.Lockfile.unlock = sinon.stub().callsArgWith(1, null); + return this.LockManager.runWithLock(this.lockFile, this.runner, this.callback); + }); + + it("should not run the compile", function() { + return this.runner .called - .should.equal false + .should.equal(false); + }); - it "should return an error", -> - error = new Errors.AlreadyCompilingError() - @callback + return it("should return an error", function() { + const error = new Errors.AlreadyCompilingError(); + return this.callback .calledWithExactly(error) - .should.equal true + .should.equal(true); + }); + }); + }); +}); diff --git a/test/unit/coffee/OutputFileFinderTests.js b/test/unit/coffee/OutputFileFinderTests.js index 46d8c1fc..3292d0a3 100644 --- a/test/unit/coffee/OutputFileFinderTests.js +++ b/test/unit/coffee/OutputFileFinderTests.js @@ -1,68 +1,92 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/OutputFileFinder' -path = require "path" -expect = require("chai").expect -EventEmitter = require("events").EventEmitter +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/OutputFileFinder'); +const path = require("path"); +const { expect } = require("chai"); +const { EventEmitter } = require("events"); -describe "OutputFileFinder", -> - beforeEach -> - @OutputFileFinder = SandboxedModule.require modulePath, requires: - "fs": @fs = {} - "child_process": spawn: @spawn = sinon.stub() +describe("OutputFileFinder", function() { + beforeEach(function() { + this.OutputFileFinder = SandboxedModule.require(modulePath, { requires: { + "fs": (this.fs = {}), + "child_process": { spawn: (this.spawn = sinon.stub()) + }, "logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() } - @directory = "/test/dir" - @callback = sinon.stub() + } + }); + this.directory = "/test/dir"; + return this.callback = sinon.stub(); + }); - describe "findOutputFiles", -> - beforeEach -> - @resource_path = "resource/path.tex" - @output_paths = ["output.pdf", "extra/file.tex"] - @all_paths = @output_paths.concat [@resource_path] - @resources = [ - path: @resource_path = "resource/path.tex" - ] - @OutputFileFinder._getAllFiles = sinon.stub().callsArgWith(1, null, @all_paths) - @OutputFileFinder.findOutputFiles @resources, @directory, (error, @outputFiles) => + describe("findOutputFiles", function() { + beforeEach(function() { + this.resource_path = "resource/path.tex"; + this.output_paths = ["output.pdf", "extra/file.tex"]; + this.all_paths = this.output_paths.concat([this.resource_path]); + this.resources = [ + {path: (this.resource_path = "resource/path.tex")} + ]; + this.OutputFileFinder._getAllFiles = sinon.stub().callsArgWith(1, null, this.all_paths); + return this.OutputFileFinder.findOutputFiles(this.resources, this.directory, (error, outputFiles) => { + this.outputFiles = outputFiles; + + }); + }); - it "should only return the output files, not directories or resource paths", -> - expect(@outputFiles).to.deep.equal [{ - path: "output.pdf" + return it("should only return the output files, not directories or resource paths", function() { + return expect(this.outputFiles).to.deep.equal([{ + path: "output.pdf", type: "pdf" }, { path: "extra/file.tex", type: "tex" - }] + }]); + }); +}); - describe "_getAllFiles", -> - beforeEach -> - @proc = new EventEmitter() - @proc.stdout = new EventEmitter() - @spawn.returns @proc - @directory = "/base/dir" - @OutputFileFinder._getAllFiles @directory, @callback + return describe("_getAllFiles", function() { + beforeEach(function() { + this.proc = new EventEmitter(); + this.proc.stdout = new EventEmitter(); + this.spawn.returns(this.proc); + this.directory = "/base/dir"; + return this.OutputFileFinder._getAllFiles(this.directory, this.callback); + }); - describe "successfully", -> - beforeEach -> - @proc.stdout.emit( + describe("successfully", function() { + beforeEach(function() { + this.proc.stdout.emit( "data", ["/base/dir/main.tex", "/base/dir/chapters/chapter1.tex"].join("\n") + "\n" - ) - @proc.emit "close", 0 + ); + return this.proc.emit("close", 0); + }); - it "should call the callback with the relative file paths", -> - @callback.calledWith( + return it("should call the callback with the relative file paths", function() { + return this.callback.calledWith( null, ["main.tex", "chapters/chapter1.tex"] - ).should.equal true + ).should.equal(true); + }); + }); - describe "when the directory doesn't exist", -> - beforeEach -> - @proc.emit "close", 1 + return describe("when the directory doesn't exist", function() { + beforeEach(function() { + return this.proc.emit("close", 1); + }); - it "should call the callback with a blank array", -> - @callback.calledWith( + return it("should call the callback with a blank array", function() { + return this.callback.calledWith( null, [] - ).should.equal true + ).should.equal(true); + }); + }); + }); +}); diff --git a/test/unit/coffee/OutputFileOptimiserTests.js b/test/unit/coffee/OutputFileOptimiserTests.js index 29887155..8934c717 100644 --- a/test/unit/coffee/OutputFileOptimiserTests.js +++ b/test/unit/coffee/OutputFileOptimiserTests.js @@ -1,103 +1,141 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/OutputFileOptimiser' -path = require "path" -expect = require("chai").expect -EventEmitter = require("events").EventEmitter - -describe "OutputFileOptimiser", -> - beforeEach -> - @OutputFileOptimiser = SandboxedModule.require modulePath, requires: - "fs": @fs = {} - "path": @Path = {} - "child_process": spawn: @spawn = sinon.stub() - "logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() } +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/OutputFileOptimiser'); +const path = require("path"); +const { expect } = require("chai"); +const { EventEmitter } = require("events"); + +describe("OutputFileOptimiser", function() { + beforeEach(function() { + this.OutputFileOptimiser = SandboxedModule.require(modulePath, { requires: { + "fs": (this.fs = {}), + "path": (this.Path = {}), + "child_process": { spawn: (this.spawn = sinon.stub()) + }, + "logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() }, "./Metrics" : {} - @directory = "/test/dir" - @callback = sinon.stub() - - describe "optimiseFile", -> - beforeEach -> - @src = "./output.pdf" - @dst = "./output.pdf" - - describe "when the file is not a pdf file", -> - beforeEach (done)-> - @src = "./output.log" - @OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false) - @OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null) - @OutputFileOptimiser.optimiseFile @src, @dst, done - - it "should not check if the file is optimised", -> - @OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(@src).should.equal false - - it "should not optimise the file", -> - @OutputFileOptimiser.optimisePDF.calledWith(@src, @dst).should.equal false - - describe "when the pdf file is not optimised", -> - beforeEach (done) -> - @OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false) - @OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null) - @OutputFileOptimiser.optimiseFile @src, @dst, done - - it "should check if the pdf is optimised", -> - @OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(@src).should.equal true - - it "should optimise the pdf", -> - @OutputFileOptimiser.optimisePDF.calledWith(@src, @dst).should.equal true - - describe "when the pdf file is optimised", -> - beforeEach (done) -> - @OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, true) - @OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null) - @OutputFileOptimiser.optimiseFile @src, @dst, done - - it "should check if the pdf is optimised", -> - @OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(@src).should.equal true - - it "should not optimise the pdf", -> - @OutputFileOptimiser.optimisePDF.calledWith(@src, @dst).should.equal false - - describe "checkIfPDFISOptimised", -> - beforeEach () -> - @callback = sinon.stub() - @fd = 1234 - @fs.open = sinon.stub().yields(null, @fd) - @fs.read = sinon.stub().withArgs(@fd).yields(null, 100, new Buffer("hello /Linearized 1")) - @fs.close = sinon.stub().withArgs(@fd).yields(null) - @OutputFileOptimiser.checkIfPDFIsOptimised @src, @callback - - describe "for a linearised file", -> - beforeEach () -> - @fs.read = sinon.stub().withArgs(@fd).yields(null, 100, new Buffer("hello /Linearized 1")) - @OutputFileOptimiser.checkIfPDFIsOptimised @src, @callback - - it "should open the file", -> - @fs.open.calledWith(@src, "r").should.equal true - - it "should read the header", -> - @fs.read.calledWith(@fd).should.equal true - - it "should close the file", -> - @fs.close.calledWith(@fd).should.equal true - - it "should call the callback with a true result", -> - @callback.calledWith(null, true).should.equal true - - describe "for an unlinearised file", -> - beforeEach () -> - @fs.read = sinon.stub().withArgs(@fd).yields(null, 100, new Buffer("hello not linearized 1")) - @OutputFileOptimiser.checkIfPDFIsOptimised @src, @callback - - it "should open the file", -> - @fs.open.calledWith(@src, "r").should.equal true - - it "should read the header", -> - @fs.read.calledWith(@fd).should.equal true - - it "should close the file", -> - @fs.close.calledWith(@fd).should.equal true - - it "should call the callback with a false result", -> - @callback.calledWith(null, false).should.equal true + } + }); + this.directory = "/test/dir"; + return this.callback = sinon.stub(); + }); + + describe("optimiseFile", function() { + beforeEach(function() { + this.src = "./output.pdf"; + return this.dst = "./output.pdf"; + }); + + describe("when the file is not a pdf file", function() { + beforeEach(function(done){ + this.src = "./output.log"; + this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false); + this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null); + return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done); + }); + + it("should not check if the file is optimised", function() { + return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(false); + }); + + return it("should not optimise the file", function() { + return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(false); + }); + }); + + describe("when the pdf file is not optimised", function() { + beforeEach(function(done) { + this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false); + this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null); + return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done); + }); + + it("should check if the pdf is optimised", function() { + return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(true); + }); + + return it("should optimise the pdf", function() { + return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(true); + }); + }); + + return describe("when the pdf file is optimised", function() { + beforeEach(function(done) { + this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, true); + this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null); + return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done); + }); + + it("should check if the pdf is optimised", function() { + return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(true); + }); + + return it("should not optimise the pdf", function() { + return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(false); + }); + }); + }); + + return describe("checkIfPDFISOptimised", function() { + beforeEach(function() { + this.callback = sinon.stub(); + this.fd = 1234; + this.fs.open = sinon.stub().yields(null, this.fd); + this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello /Linearized 1")); + this.fs.close = sinon.stub().withArgs(this.fd).yields(null); + return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback); + }); + + describe("for a linearised file", function() { + beforeEach(function() { + this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello /Linearized 1")); + return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback); + }); + + it("should open the file", function() { + return this.fs.open.calledWith(this.src, "r").should.equal(true); + }); + + it("should read the header", function() { + return this.fs.read.calledWith(this.fd).should.equal(true); + }); + + it("should close the file", function() { + return this.fs.close.calledWith(this.fd).should.equal(true); + }); + + return it("should call the callback with a true result", function() { + return this.callback.calledWith(null, true).should.equal(true); + }); + }); + + return describe("for an unlinearised file", function() { + beforeEach(function() { + this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello not linearized 1")); + return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback); + }); + + it("should open the file", function() { + return this.fs.open.calledWith(this.src, "r").should.equal(true); + }); + + it("should read the header", function() { + return this.fs.read.calledWith(this.fd).should.equal(true); + }); + + it("should close the file", function() { + return this.fs.close.calledWith(this.fd).should.equal(true); + }); + + return it("should call the callback with a false result", function() { + return this.callback.calledWith(null, false).should.equal(true); + }); + }); + }); +}); diff --git a/test/unit/coffee/ProjectPersistenceManagerTests.js b/test/unit/coffee/ProjectPersistenceManagerTests.js index 69bfd4fa..c15cd808 100644 --- a/test/unit/coffee/ProjectPersistenceManagerTests.js +++ b/test/unit/coffee/ProjectPersistenceManagerTests.js @@ -1,62 +1,82 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/ProjectPersistenceManager' -tk = require("timekeeper") - -describe "ProjectPersistenceManager", -> - beforeEach -> - @ProjectPersistenceManager = SandboxedModule.require modulePath, requires: - "./UrlCache": @UrlCache = {} - "./CompileManager": @CompileManager = {} - "logger-sharelatex": @logger = { log: sinon.stub() } - "./db": @db = {} - @callback = sinon.stub() - @project_id = "project-id-123" - @user_id = "1234" - - describe "clearExpiredProjects", -> - beforeEach -> - @project_ids = [ - "project-id-1" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/ProjectPersistenceManager'); +const tk = require("timekeeper"); + +describe("ProjectPersistenceManager", function() { + beforeEach(function() { + this.ProjectPersistenceManager = SandboxedModule.require(modulePath, { requires: { + "./UrlCache": (this.UrlCache = {}), + "./CompileManager": (this.CompileManager = {}), + "logger-sharelatex": (this.logger = { log: sinon.stub() }), + "./db": (this.db = {}) + } + }); + this.callback = sinon.stub(); + this.project_id = "project-id-123"; + return this.user_id = "1234"; + }); + + describe("clearExpiredProjects", function() { + beforeEach(function() { + this.project_ids = [ + "project-id-1", "project-id-2" - ] - @ProjectPersistenceManager._findExpiredProjectIds = sinon.stub().callsArgWith(0, null, @project_ids) - @ProjectPersistenceManager.clearProjectFromCache = sinon.stub().callsArg(1) - @CompileManager.clearExpiredProjects = sinon.stub().callsArg(1) - @ProjectPersistenceManager.clearExpiredProjects @callback - - it "should clear each expired project", -> - for project_id in @project_ids - @ProjectPersistenceManager.clearProjectFromCache + ]; + this.ProjectPersistenceManager._findExpiredProjectIds = sinon.stub().callsArgWith(0, null, this.project_ids); + this.ProjectPersistenceManager.clearProjectFromCache = sinon.stub().callsArg(1); + this.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1); + return this.ProjectPersistenceManager.clearExpiredProjects(this.callback); + }); + + it("should clear each expired project", function() { + return Array.from(this.project_ids).map((project_id) => + this.ProjectPersistenceManager.clearProjectFromCache .calledWith(project_id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "clearProject", -> - beforeEach -> - @ProjectPersistenceManager._clearProjectFromDatabase = sinon.stub().callsArg(1) - @UrlCache.clearProject = sinon.stub().callsArg(1) - @CompileManager.clearProject = sinon.stub().callsArg(2) - @ProjectPersistenceManager.clearProject @project_id, @user_id, @callback - - it "should clear the project from the database", -> - @ProjectPersistenceManager._clearProjectFromDatabase - .calledWith(@project_id) - .should.equal true - - it "should clear all the cached Urls for the project", -> - @UrlCache.clearProject - .calledWith(@project_id) - .should.equal true - - it "should clear the project compile folder", -> - @CompileManager.clearProject - .calledWith(@project_id, @user_id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true + .should.equal(true)); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + return describe("clearProject", function() { + beforeEach(function() { + this.ProjectPersistenceManager._clearProjectFromDatabase = sinon.stub().callsArg(1); + this.UrlCache.clearProject = sinon.stub().callsArg(1); + this.CompileManager.clearProject = sinon.stub().callsArg(2); + return this.ProjectPersistenceManager.clearProject(this.project_id, this.user_id, this.callback); + }); + + it("should clear the project from the database", function() { + return this.ProjectPersistenceManager._clearProjectFromDatabase + .calledWith(this.project_id) + .should.equal(true); + }); + + it("should clear all the cached Urls for the project", function() { + return this.UrlCache.clearProject + .calledWith(this.project_id) + .should.equal(true); + }); + + it("should clear the project compile folder", function() { + return this.CompileManager.clearProject + .calledWith(this.project_id, this.user_id) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); +}); diff --git a/test/unit/coffee/RequestParserTests.js b/test/unit/coffee/RequestParserTests.js index e263e492..5ca09411 100644 --- a/test/unit/coffee/RequestParserTests.js +++ b/test/unit/coffee/RequestParserTests.js @@ -1,279 +1,380 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -expect = require('chai').expect -modulePath = require('path').join __dirname, '../../../app/js/RequestParser' -tk = require("timekeeper") - -describe "RequestParser", -> - beforeEach -> - tk.freeze() - @callback = sinon.stub() - @validResource = - path: "main.tex" - date: "12:00 01/02/03" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const { expect } = require('chai'); +const modulePath = require('path').join(__dirname, '../../../app/js/RequestParser'); +const tk = require("timekeeper"); + +describe("RequestParser", function() { + beforeEach(function() { + tk.freeze(); + this.callback = sinon.stub(); + this.validResource = { + path: "main.tex", + date: "12:00 01/02/03", content: "Hello world" - @validRequest = - compile: - token: "token-123" - options: - imageName: "basicImageName/here:2017-1" - compiler: "pdflatex" + }; + this.validRequest = { + compile: { + token: "token-123", + options: { + imageName: "basicImageName/here:2017-1", + compiler: "pdflatex", timeout: 42 + }, resources: [] - @RequestParser = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = {} - - afterEach -> - tk.reset() - - describe "without a top level object", -> - beforeEach -> - @RequestParser.parse [], @callback - - it "should return an error", -> - @callback.calledWith("top level object should have a compile attribute") - .should.equal true - - describe "without a compile attribute", -> - beforeEach -> - @RequestParser.parse {}, @callback - - it "should return an error", -> - @callback.calledWith("top level object should have a compile attribute") - .should.equal true - - describe "without a valid compiler", -> - beforeEach -> - @validRequest.compile.options.compiler = "not-a-compiler" - @RequestParser.parse @validRequest, @callback - - it "should return an error", -> - @callback.calledWith("compiler attribute should be one of: pdflatex, latex, xelatex, lualatex") - .should.equal true - - describe "without a compiler specified", -> - beforeEach -> - delete @validRequest.compile.options.compiler - @RequestParser.parse @validRequest, (error, @data) => - - it "should set the compiler to pdflatex by default", -> - @data.compiler.should.equal "pdflatex" - - describe "with imageName set", -> - beforeEach -> - @RequestParser.parse @validRequest, (error, @data) => - - it "should set the imageName", -> - @data.imageName.should.equal "basicImageName/here:2017-1" - - describe "with flags set", -> - beforeEach -> - @validRequest.compile.options.flags = ["-file-line-error"] - @RequestParser.parse @validRequest, (error, @data) => - - it "should set the flags attribute", -> - expect(@data.flags).to.deep.equal ["-file-line-error"] - - describe "with flags not specified", -> - beforeEach -> - @RequestParser.parse @validRequest, (error, @data) => - - it "it should have an empty flags list", -> - expect(@data.flags).to.deep.equal [] - - describe "without a timeout specified", -> - beforeEach -> - delete @validRequest.compile.options.timeout - @RequestParser.parse @validRequest, (error, @data) => - - it "should set the timeout to MAX_TIMEOUT", -> - @data.timeout.should.equal @RequestParser.MAX_TIMEOUT * 1000 - - describe "with a timeout larger than the maximum", -> - beforeEach -> - @validRequest.compile.options.timeout = @RequestParser.MAX_TIMEOUT + 1 - @RequestParser.parse @validRequest, (error, @data) => - - it "should set the timeout to MAX_TIMEOUT", -> - @data.timeout.should.equal @RequestParser.MAX_TIMEOUT * 1000 - - describe "with a timeout", -> - beforeEach -> - @RequestParser.parse @validRequest, (error, @data) => - - it "should set the timeout (in milliseconds)", -> - @data.timeout.should.equal @validRequest.compile.options.timeout * 1000 - - describe "with a resource without a path", -> - beforeEach -> - delete @validResource.path - @validRequest.compile.resources.push @validResource - @RequestParser.parse @validRequest, @callback - - it "should return an error", -> - @callback.calledWith("all resources should have a path attribute") - .should.equal true - - describe "with a resource with a path", -> - beforeEach -> - @validResource.path = @path = "test.tex" - @validRequest.compile.resources.push @validResource - @RequestParser.parse @validRequest, @callback - @data = @callback.args[0][1] - - it "should return the path in the parsed response", -> - @data.resources[0].path.should.equal @path - - describe "with a resource with a malformed modified date", -> - beforeEach -> - @validResource.modified = "not-a-date" - @validRequest.compile.resources.push @validResource - @RequestParser.parse @validRequest, @callback - - it "should return an error", -> - @callback + } + }; + return this.RequestParser = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.settings = {}) + } + });}); + + afterEach(() => tk.reset()); + + describe("without a top level object", function() { + beforeEach(function() { + return this.RequestParser.parse([], this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("top level object should have a compile attribute") + .should.equal(true); + }); + }); + + describe("without a compile attribute", function() { + beforeEach(function() { + return this.RequestParser.parse({}, this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("top level object should have a compile attribute") + .should.equal(true); + }); + }); + + describe("without a valid compiler", function() { + beforeEach(function() { + this.validRequest.compile.options.compiler = "not-a-compiler"; + return this.RequestParser.parse(this.validRequest, this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("compiler attribute should be one of: pdflatex, latex, xelatex, lualatex") + .should.equal(true); + }); + }); + + describe("without a compiler specified", function() { + beforeEach(function() { + delete this.validRequest.compile.options.compiler; + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data; + + }); + }); + + return it("should set the compiler to pdflatex by default", function() { + return this.data.compiler.should.equal("pdflatex"); + }); + }); + + describe("with imageName set", function() { + beforeEach(function() { + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data; + + }); + }); + + return it("should set the imageName", function() { + return this.data.imageName.should.equal("basicImageName/here:2017-1"); + }); + }); + + describe("with flags set", function() { + beforeEach(function() { + this.validRequest.compile.options.flags = ["-file-line-error"]; + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data; + + }); + }); + + return it("should set the flags attribute", function() { + return expect(this.data.flags).to.deep.equal(["-file-line-error"]); + }); +}); + + describe("with flags not specified", function() { + beforeEach(function() { + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data; + + }); + }); + + return it("it should have an empty flags list", function() { + return expect(this.data.flags).to.deep.equal([]); + }); +}); + + describe("without a timeout specified", function() { + beforeEach(function() { + delete this.validRequest.compile.options.timeout; + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data; + + }); + }); + + return it("should set the timeout to MAX_TIMEOUT", function() { + return this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000); + }); + }); + + describe("with a timeout larger than the maximum", function() { + beforeEach(function() { + this.validRequest.compile.options.timeout = this.RequestParser.MAX_TIMEOUT + 1; + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data; + + }); + }); + + return it("should set the timeout to MAX_TIMEOUT", function() { + return this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000); + }); + }); + + describe("with a timeout", function() { + beforeEach(function() { + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data; + + }); + }); + + return it("should set the timeout (in milliseconds)", function() { + return this.data.timeout.should.equal(this.validRequest.compile.options.timeout * 1000); + }); + }); + + describe("with a resource without a path", function() { + beforeEach(function() { + delete this.validResource.path; + this.validRequest.compile.resources.push(this.validResource); + return this.RequestParser.parse(this.validRequest, this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("all resources should have a path attribute") + .should.equal(true); + }); + }); + + describe("with a resource with a path", function() { + beforeEach(function() { + this.validResource.path = (this.path = "test.tex"); + this.validRequest.compile.resources.push(this.validResource); + this.RequestParser.parse(this.validRequest, this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return the path in the parsed response", function() { + return this.data.resources[0].path.should.equal(this.path); + }); + }); + + describe("with a resource with a malformed modified date", function() { + beforeEach(function() { + this.validResource.modified = "not-a-date"; + this.validRequest.compile.resources.push(this.validResource); + return this.RequestParser.parse(this.validRequest, this.callback); + }); + + return it("should return an error", function() { + return this.callback .calledWith( "resource modified date could not be understood: "+ - @validResource.modified + this.validResource.modified ) - .should.equal true - - describe "with a resource with a valid date", -> - beforeEach -> - @date = "12:00 01/02/03" - @validResource.modified = @date - @validRequest.compile.resources.push @validResource - @RequestParser.parse @validRequest, @callback - @data = @callback.args[0][1] - - it "should return the date as a Javascript Date object", -> - (@data.resources[0].modified instanceof Date).should.equal true - @data.resources[0].modified.getTime().should.equal Date.parse(@date) - - describe "with a resource without either a content or URL attribute", -> - beforeEach -> - delete @validResource.url - delete @validResource.content - @validRequest.compile.resources.push @validResource - @RequestParser.parse @validRequest, @callback - - it "should return an error", -> - @callback.calledWith("all resources should have either a url or content attribute") - .should.equal true - - describe "with a resource where the content is not a string", -> - beforeEach -> - @validResource.content = [] - @validRequest.compile.resources.push @validResource - @RequestParser.parse (@validRequest), @callback - - it "should return an error", -> - @callback.calledWith("content attribute should be a string") - .should.equal true - - describe "with a resource where the url is not a string", -> - beforeEach -> - @validResource.url = [] - @validRequest.compile.resources.push @validResource - @RequestParser.parse (@validRequest), @callback - - it "should return an error", -> - @callback.calledWith("url attribute should be a string") - .should.equal true - - describe "with a resource with a url", -> - beforeEach -> - @validResource.url = @url = "www.example.com" - @validRequest.compile.resources.push @validResource - @RequestParser.parse (@validRequest), @callback - @data = @callback.args[0][1] - - it "should return the url in the parsed response", -> - @data.resources[0].url.should.equal @url - - describe "with a resource with a content attribute", -> - beforeEach -> - @validResource.content = @content = "Hello world" - @validRequest.compile.resources.push @validResource - @RequestParser.parse (@validRequest), @callback - @data = @callback.args[0][1] - - it "should return the content in the parsed response", -> - @data.resources[0].content.should.equal @content - - describe "without a root resource path", -> - beforeEach -> - delete @validRequest.compile.rootResourcePath - @RequestParser.parse (@validRequest), @callback - @data = @callback.args[0][1] - - it "should set the root resource path to 'main.tex' by default", -> - @data.rootResourcePath.should.equal "main.tex" - - describe "with a root resource path", -> - beforeEach -> - @validRequest.compile.rootResourcePath = @path = "test.tex" - @RequestParser.parse (@validRequest), @callback - @data = @callback.args[0][1] - - it "should return the root resource path in the parsed response", -> - @data.rootResourcePath.should.equal @path - - describe "with a root resource path that is not a string", -> - beforeEach -> - @validRequest.compile.rootResourcePath = [] - @RequestParser.parse (@validRequest), @callback - - it "should return an error", -> - @callback.calledWith("rootResourcePath attribute should be a string") - .should.equal true - - describe "with a root resource path that needs escaping", -> - beforeEach -> - @badPath = "`rm -rf foo`.tex" - @goodPath = "rm -rf foo.tex" - @validRequest.compile.rootResourcePath = @badPath - @validRequest.compile.resources.push { - path: @badPath - date: "12:00 01/02/03" + .should.equal(true); + }); + }); + + describe("with a resource with a valid date", function() { + beforeEach(function() { + this.date = "12:00 01/02/03"; + this.validResource.modified = this.date; + this.validRequest.compile.resources.push(this.validResource); + this.RequestParser.parse(this.validRequest, this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return the date as a Javascript Date object", function() { + (this.data.resources[0].modified instanceof Date).should.equal(true); + return this.data.resources[0].modified.getTime().should.equal(Date.parse(this.date)); + }); + }); + + describe("with a resource without either a content or URL attribute", function() { + beforeEach(function() { + delete this.validResource.url; + delete this.validResource.content; + this.validRequest.compile.resources.push(this.validResource); + return this.RequestParser.parse(this.validRequest, this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("all resources should have either a url or content attribute") + .should.equal(true); + }); + }); + + describe("with a resource where the content is not a string", function() { + beforeEach(function() { + this.validResource.content = []; + this.validRequest.compile.resources.push(this.validResource); + return this.RequestParser.parse((this.validRequest), this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("content attribute should be a string") + .should.equal(true); + }); + }); + + describe("with a resource where the url is not a string", function() { + beforeEach(function() { + this.validResource.url = []; + this.validRequest.compile.resources.push(this.validResource); + return this.RequestParser.parse((this.validRequest), this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("url attribute should be a string") + .should.equal(true); + }); + }); + + describe("with a resource with a url", function() { + beforeEach(function() { + this.validResource.url = (this.url = "www.example.com"); + this.validRequest.compile.resources.push(this.validResource); + this.RequestParser.parse((this.validRequest), this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return the url in the parsed response", function() { + return this.data.resources[0].url.should.equal(this.url); + }); + }); + + describe("with a resource with a content attribute", function() { + beforeEach(function() { + this.validResource.content = (this.content = "Hello world"); + this.validRequest.compile.resources.push(this.validResource); + this.RequestParser.parse((this.validRequest), this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return the content in the parsed response", function() { + return this.data.resources[0].content.should.equal(this.content); + }); + }); + + describe("without a root resource path", function() { + beforeEach(function() { + delete this.validRequest.compile.rootResourcePath; + this.RequestParser.parse((this.validRequest), this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should set the root resource path to 'main.tex' by default", function() { + return this.data.rootResourcePath.should.equal("main.tex"); + }); + }); + + describe("with a root resource path", function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = (this.path = "test.tex"); + this.RequestParser.parse((this.validRequest), this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return the root resource path in the parsed response", function() { + return this.data.rootResourcePath.should.equal(this.path); + }); + }); + + describe("with a root resource path that is not a string", function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = []; + return this.RequestParser.parse((this.validRequest), this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith("rootResourcePath attribute should be a string") + .should.equal(true); + }); + }); + + describe("with a root resource path that needs escaping", function() { + beforeEach(function() { + this.badPath = "`rm -rf foo`.tex"; + this.goodPath = "rm -rf foo.tex"; + this.validRequest.compile.rootResourcePath = this.badPath; + this.validRequest.compile.resources.push({ + path: this.badPath, + date: "12:00 01/02/03", content: "Hello world" - } - @RequestParser.parse @validRequest, @callback - @data = @callback.args[0][1] - - it "should return the escaped resource", -> - @data.rootResourcePath.should.equal @goodPath - - it "should also escape the resource path", -> - @data.resources[0].path.should.equal @goodPath - - describe "with a root resource path that has a relative path", -> - beforeEach -> - @validRequest.compile.rootResourcePath = "foo/../../bar.tex" - @RequestParser.parse @validRequest, @callback - @data = @callback.args[0][1] - - it "should return an error", -> - @callback.calledWith("relative path in root resource") - .should.equal true - - describe "with a root resource path that has unescaped + relative path", -> - beforeEach -> - @validRequest.compile.rootResourcePath = "foo/#../bar.tex" - @RequestParser.parse @validRequest, @callback - @data = @callback.args[0][1] - - it "should return an error", -> - @callback.calledWith("relative path in root resource") - .should.equal true - - describe "with an unknown syncType", -> - beforeEach -> - @validRequest.compile.options.syncType = "unexpected" - @RequestParser.parse @validRequest, @callback - @data = @callback.args[0][1] - - it "should return an error", -> - @callback.calledWith("syncType attribute should be one of: full, incremental") - .should.equal true + }); + this.RequestParser.parse(this.validRequest, this.callback); + return this.data = this.callback.args[0][1];}); + + it("should return the escaped resource", function() { + return this.data.rootResourcePath.should.equal(this.goodPath); + }); + + return it("should also escape the resource path", function() { + return this.data.resources[0].path.should.equal(this.goodPath); + }); + }); + + describe("with a root resource path that has a relative path", function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = "foo/../../bar.tex"; + this.RequestParser.parse(this.validRequest, this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return an error", function() { + return this.callback.calledWith("relative path in root resource") + .should.equal(true); + }); + }); + + describe("with a root resource path that has unescaped + relative path", function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = "foo/#../bar.tex"; + this.RequestParser.parse(this.validRequest, this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return an error", function() { + return this.callback.calledWith("relative path in root resource") + .should.equal(true); + }); + }); + + return describe("with an unknown syncType", function() { + beforeEach(function() { + this.validRequest.compile.options.syncType = "unexpected"; + this.RequestParser.parse(this.validRequest, this.callback); + return this.data = this.callback.args[0][1];}); + + return it("should return an error", function() { + return this.callback.calledWith("syncType attribute should be one of: full, incremental") + .should.equal(true); + }); + }); +}); diff --git a/test/unit/coffee/ResourceStateManagerTests.js b/test/unit/coffee/ResourceStateManagerTests.js index e5e1c130..4b09135e 100644 --- a/test/unit/coffee/ResourceStateManagerTests.js +++ b/test/unit/coffee/ResourceStateManagerTests.js @@ -1,109 +1,147 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -should = require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/ResourceStateManager' -Path = require "path" -Errors = require "../../../app/js/Errors" - -describe "ResourceStateManager", -> - beforeEach -> - @ResourceStateManager = SandboxedModule.require modulePath, requires: - "fs": @fs = {} - "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()} - "./SafeReader": @SafeReader = {} - @basePath = "/path/to/write/files/to" - @resources = [ - {path: "resource-1-mock"} - {path: "resource-2-mock"} +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +const should = require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/ResourceStateManager'); +const Path = require("path"); +const Errors = require("../../../app/js/Errors"); + +describe("ResourceStateManager", function() { + beforeEach(function() { + this.ResourceStateManager = SandboxedModule.require(modulePath, { requires: { + "fs": (this.fs = {}), + "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()}, + "./SafeReader": (this.SafeReader = {}) + } + }); + this.basePath = "/path/to/write/files/to"; + this.resources = [ + {path: "resource-1-mock"}, + {path: "resource-2-mock"}, {path: "resource-3-mock"} - ] - @state = "1234567890" - @resourceFileName = "#{@basePath}/.project-sync-state" - @resourceFileContents = "#{@resources[0].path}\n#{@resources[1].path}\n#{@resources[2].path}\nstateHash:#{@state}" - @callback = sinon.stub() - - describe "saveProjectState", -> - beforeEach -> - @fs.writeFile = sinon.stub().callsArg(2) - - describe "when the state is specified", -> - beforeEach -> - @ResourceStateManager.saveProjectState(@state, @resources, @basePath, @callback) - - it "should write the resource list to disk", -> - @fs.writeFile - .calledWith(@resourceFileName, @resourceFileContents) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the state is undefined", -> - beforeEach -> - @state = undefined - @fs.unlink = sinon.stub().callsArg(1) - @ResourceStateManager.saveProjectState(@state, @resources, @basePath, @callback) - - it "should unlink the resource file", -> - @fs.unlink - .calledWith(@resourceFileName) - .should.equal true - - it "should not write the resource list to disk", -> - @fs.writeFile.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - describe "checkProjectStateMatches", -> - - describe "when the state matches", -> - beforeEach -> - @SafeReader.readFile = sinon.stub().callsArgWith(3, null, @resourceFileContents) - @ResourceStateManager.checkProjectStateMatches(@state, @basePath, @callback) - - it "should read the resource file", -> - @SafeReader.readFile - .calledWith(@resourceFileName) - .should.equal true - - it "should call the callback with the results", -> - @callback.calledWithMatch(null, @resources).should.equal true - - describe "when the state does not match", -> - beforeEach -> - @SafeReader.readFile = sinon.stub().callsArgWith(3, null, @resourceFileContents) - @ResourceStateManager.checkProjectStateMatches("not-the-original-state", @basePath, @callback) - - it "should call the callback with an error", -> - error = new Errors.FilesOutOfSyncError("invalid state for incremental update") - @callback.calledWith(error).should.equal true - - describe "checkResourceFiles", -> - describe "when all the files are present", -> - beforeEach -> - @allFiles = [ @resources[0].path, @resources[1].path, @resources[2].path] - @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) - - it "should call the callback", -> - @callback.calledWithExactly().should.equal true - - describe "when there is a missing file", -> - beforeEach -> - @allFiles = [ @resources[0].path, @resources[1].path] - @fs.stat = sinon.stub().callsArgWith(1, new Error()) - @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) - - it "should call the callback with an error", -> - error = new Errors.FilesOutOfSyncError("resource files missing in incremental update") - @callback.calledWith(error).should.equal true - - describe "when a resource contains a relative path", -> - beforeEach -> - @resources[0].path = "../foo/bar.tex" - @allFiles = [ @resources[0].path, @resources[1].path, @resources[2].path] - @ResourceStateManager.checkResourceFiles(@resources, @allFiles, @basePath, @callback) - - it "should call the callback with an error", -> - @callback.calledWith(new Error("relative path in resource file list")).should.equal true + ]; + this.state = "1234567890"; + this.resourceFileName = `${this.basePath}/.project-sync-state`; + this.resourceFileContents = `${this.resources[0].path}\n${this.resources[1].path}\n${this.resources[2].path}\nstateHash:${this.state}`; + return this.callback = sinon.stub(); + }); + + describe("saveProjectState", function() { + beforeEach(function() { + return this.fs.writeFile = sinon.stub().callsArg(2); + }); + + describe("when the state is specified", function() { + beforeEach(function() { + return this.ResourceStateManager.saveProjectState(this.state, this.resources, this.basePath, this.callback); + }); + + it("should write the resource list to disk", function() { + return this.fs.writeFile + .calledWith(this.resourceFileName, this.resourceFileContents) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + return describe("when the state is undefined", function() { + beforeEach(function() { + this.state = undefined; + this.fs.unlink = sinon.stub().callsArg(1); + return this.ResourceStateManager.saveProjectState(this.state, this.resources, this.basePath, this.callback); + }); + + it("should unlink the resource file", function() { + return this.fs.unlink + .calledWith(this.resourceFileName) + .should.equal(true); + }); + + it("should not write the resource list to disk", function() { + return this.fs.writeFile.called.should.equal(false); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + }); + + describe("checkProjectStateMatches", function() { + + describe("when the state matches", function() { + beforeEach(function() { + this.SafeReader.readFile = sinon.stub().callsArgWith(3, null, this.resourceFileContents); + return this.ResourceStateManager.checkProjectStateMatches(this.state, this.basePath, this.callback); + }); + + it("should read the resource file", function() { + return this.SafeReader.readFile + .calledWith(this.resourceFileName) + .should.equal(true); + }); + + return it("should call the callback with the results", function() { + return this.callback.calledWithMatch(null, this.resources).should.equal(true); + }); + }); + + return describe("when the state does not match", function() { + beforeEach(function() { + this.SafeReader.readFile = sinon.stub().callsArgWith(3, null, this.resourceFileContents); + return this.ResourceStateManager.checkProjectStateMatches("not-the-original-state", this.basePath, this.callback); + }); + + return it("should call the callback with an error", function() { + const error = new Errors.FilesOutOfSyncError("invalid state for incremental update"); + return this.callback.calledWith(error).should.equal(true); + }); + }); + }); + + return describe("checkResourceFiles", function() { + describe("when all the files are present", function() { + beforeEach(function() { + this.allFiles = [ this.resources[0].path, this.resources[1].path, this.resources[2].path]; + return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback); + }); + + return it("should call the callback", function() { + return this.callback.calledWithExactly().should.equal(true); + }); + }); + + describe("when there is a missing file", function() { + beforeEach(function() { + this.allFiles = [ this.resources[0].path, this.resources[1].path]; + this.fs.stat = sinon.stub().callsArgWith(1, new Error()); + return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback); + }); + + return it("should call the callback with an error", function() { + const error = new Errors.FilesOutOfSyncError("resource files missing in incremental update"); + return this.callback.calledWith(error).should.equal(true); + }); + }); + + return describe("when a resource contains a relative path", function() { + beforeEach(function() { + this.resources[0].path = "../foo/bar.tex"; + this.allFiles = [ this.resources[0].path, this.resources[1].path, this.resources[2].path]; + return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback); + }); + + return it("should call the callback with an error", function() { + return this.callback.calledWith(new Error("relative path in resource file list")).should.equal(true); + }); + }); + }); +}); diff --git a/test/unit/coffee/ResourceWriterTests.js b/test/unit/coffee/ResourceWriterTests.js index 4a88226f..89433c82 100644 --- a/test/unit/coffee/ResourceWriterTests.js +++ b/test/unit/coffee/ResourceWriterTests.js @@ -1,324 +1,409 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -should = require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/ResourceWriter' -path = require "path" - -describe "ResourceWriter", -> - beforeEach -> - @ResourceWriter = SandboxedModule.require modulePath, requires: - "fs": @fs = - mkdir: sinon.stub().callsArg(1) +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +const should = require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/ResourceWriter'); +const path = require("path"); + +describe("ResourceWriter", function() { + beforeEach(function() { + let Timer; + this.ResourceWriter = SandboxedModule.require(modulePath, { requires: { + "fs": (this.fs = { + mkdir: sinon.stub().callsArg(1), unlink: sinon.stub().callsArg(1) - "./ResourceStateManager": @ResourceStateManager = {} - "wrench": @wrench = {} - "./UrlCache" : @UrlCache = {} - "mkdirp" : @mkdirp = sinon.stub().callsArg(1) - "./OutputFileFinder": @OutputFileFinder = {} - "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()} - "./Metrics": @Metrics = - Timer: class Timer - done: sinon.stub() - @project_id = "project-id-123" - @basePath = "/path/to/write/files/to" - @callback = sinon.stub() - - describe "syncResourcesToDisk on a full request", -> - beforeEach -> - @resources = [ - "resource-1-mock" - "resource-2-mock" + }), + "./ResourceStateManager": (this.ResourceStateManager = {}), + "wrench": (this.wrench = {}), + "./UrlCache" : (this.UrlCache = {}), + "mkdirp" : (this.mkdirp = sinon.stub().callsArg(1)), + "./OutputFileFinder": (this.OutputFileFinder = {}), + "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()}, + "./Metrics": (this.Metrics = { + Timer: (Timer = (function() { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub(); + } + }; + Timer.initClass(); + return Timer; + })()) + }) + } + } + ); + this.project_id = "project-id-123"; + this.basePath = "/path/to/write/files/to"; + return this.callback = sinon.stub(); + }); + + describe("syncResourcesToDisk on a full request", function() { + beforeEach(function() { + this.resources = [ + "resource-1-mock", + "resource-2-mock", "resource-3-mock" - ] - @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) - @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceStateManager.saveProjectState = sinon.stub().callsArg(3) - @ResourceWriter.syncResourcesToDisk({ - project_id: @project_id - syncState: @syncState = "0123456789abcdef" - resources: @resources - }, @basePath, @callback) - - it "should remove old files", -> - @ResourceWriter._removeExtraneousFiles - .calledWith(@resources, @basePath) - .should.equal true - - it "should write each resource to disk", -> - for resource in @resources - @ResourceWriter._writeResourceToDisk - .calledWith(@project_id, resource, @basePath) - .should.equal true - - it "should store the sync state and resource list", -> - @ResourceStateManager.saveProjectState - .calledWith(@syncState, @resources, @basePath) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "syncResourcesToDisk on an incremental update", -> - beforeEach -> - @resources = [ + ]; + this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3); + this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2); + this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3); + return this.ResourceWriter.syncResourcesToDisk({ + project_id: this.project_id, + syncState: (this.syncState = "0123456789abcdef"), + resources: this.resources + }, this.basePath, this.callback); + }); + + it("should remove old files", function() { + return this.ResourceWriter._removeExtraneousFiles + .calledWith(this.resources, this.basePath) + .should.equal(true); + }); + + it("should write each resource to disk", function() { + return Array.from(this.resources).map((resource) => + this.ResourceWriter._writeResourceToDisk + .calledWith(this.project_id, resource, this.basePath) + .should.equal(true)); + }); + + it("should store the sync state and resource list", function() { + return this.ResourceStateManager.saveProjectState + .calledWith(this.syncState, this.resources, this.basePath) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + describe("syncResourcesToDisk on an incremental update", function() { + beforeEach(function() { + this.resources = [ "resource-1-mock" - ] - @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) - @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArgWith(2, null, @outputFiles = [], @allFiles = []) - @ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, null, @resources) - @ResourceStateManager.saveProjectState = sinon.stub().callsArg(3) - @ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3) - @ResourceWriter.syncResourcesToDisk({ - project_id: @project_id, + ]; + this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3); + this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArgWith(2, null, (this.outputFiles = []), (this.allFiles = [])); + this.ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, null, this.resources); + this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3); + this.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3); + return this.ResourceWriter.syncResourcesToDisk({ + project_id: this.project_id, syncType: "incremental", - syncState: @syncState = "1234567890abcdef", - resources: @resources - }, @basePath, @callback) - - it "should check the sync state matches", -> - @ResourceStateManager.checkProjectStateMatches - .calledWith(@syncState, @basePath) - .should.equal true - - it "should remove old files", -> - @ResourceWriter._removeExtraneousFiles - .calledWith(@resources, @basePath) - .should.equal true - - it "should check each resource exists", -> - @ResourceStateManager.checkResourceFiles - .calledWith(@resources, @allFiles, @basePath) - .should.equal true - - it "should write each resource to disk", -> - for resource in @resources - @ResourceWriter._writeResourceToDisk - .calledWith(@project_id, resource, @basePath) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "syncResourcesToDisk on an incremental update when the state does not match", -> - beforeEach -> - @resources = [ + syncState: (this.syncState = "1234567890abcdef"), + resources: this.resources + }, this.basePath, this.callback); + }); + + it("should check the sync state matches", function() { + return this.ResourceStateManager.checkProjectStateMatches + .calledWith(this.syncState, this.basePath) + .should.equal(true); + }); + + it("should remove old files", function() { + return this.ResourceWriter._removeExtraneousFiles + .calledWith(this.resources, this.basePath) + .should.equal(true); + }); + + it("should check each resource exists", function() { + return this.ResourceStateManager.checkResourceFiles + .calledWith(this.resources, this.allFiles, this.basePath) + .should.equal(true); + }); + + it("should write each resource to disk", function() { + return Array.from(this.resources).map((resource) => + this.ResourceWriter._writeResourceToDisk + .calledWith(this.project_id, resource, this.basePath) + .should.equal(true)); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + describe("syncResourcesToDisk on an incremental update when the state does not match", function() { + beforeEach(function() { + this.resources = [ "resource-1-mock" - ] - @ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, @error = new Error()) - @ResourceWriter.syncResourcesToDisk({ - project_id: @project_id, + ]; + this.ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, (this.error = new Error())); + return this.ResourceWriter.syncResourcesToDisk({ + project_id: this.project_id, syncType: "incremental", - syncState: @syncState = "1234567890abcdef", - resources: @resources - }, @basePath, @callback) - - it "should check whether the sync state matches", -> - @ResourceStateManager.checkProjectStateMatches - .calledWith(@syncState, @basePath) - .should.equal true - - it "should call the callback with an error", -> - @callback.calledWith(@error).should.equal true - - - describe "_removeExtraneousFiles", -> - beforeEach -> - @output_files = [{ - path: "output.pdf" + syncState: (this.syncState = "1234567890abcdef"), + resources: this.resources + }, this.basePath, this.callback); + }); + + it("should check whether the sync state matches", function() { + return this.ResourceStateManager.checkProjectStateMatches + .calledWith(this.syncState, this.basePath) + .should.equal(true); + }); + + return it("should call the callback with an error", function() { + return this.callback.calledWith(this.error).should.equal(true); + }); + }); + + + describe("_removeExtraneousFiles", function() { + beforeEach(function() { + this.output_files = [{ + path: "output.pdf", type: "pdf" }, { - path: "extra/file.tex" + path: "extra/file.tex", type: "tex" }, { - path: "extra.aux" + path: "extra.aux", type: "aux" }, { path: "cache/_chunk1" },{ - path: "figures/image-eps-converted-to.pdf" + path: "figures/image-eps-converted-to.pdf", type: "pdf" },{ - path: "foo/main-figure0.md5" + path: "foo/main-figure0.md5", type: "md5" }, { - path: "foo/main-figure0.dpth" + path: "foo/main-figure0.dpth", type: "dpth" }, { - path: "foo/main-figure0.pdf" + path: "foo/main-figure0.pdf", type: "pdf" }, { - path: "_minted-main/default-pyg-prefix.pygstyle" + path: "_minted-main/default-pyg-prefix.pygstyle", type: "pygstyle" }, { - path: "_minted-main/default.pygstyle" + path: "_minted-main/default.pygstyle", type: "pygstyle" }, { - path: "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex" + path: "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex", type: "pygtex" }, { - path: "_markdown_main/30893013dec5d869a415610079774c2f.md.tex" + path: "_markdown_main/30893013dec5d869a415610079774c2f.md.tex", type: "tex" - }] - @resources = "mock-resources" - @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files) - @ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1) - @ResourceWriter._removeExtraneousFiles(@resources, @basePath, @callback) - - it "should find the existing output files", -> - @OutputFileFinder.findOutputFiles - .calledWith(@resources, @basePath) - .should.equal true - - it "should delete the output files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "output.pdf")) - .should.equal true - - it "should delete the extra files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "extra/file.tex")) - .should.equal true - - it "should not delete the extra aux files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "extra.aux")) - .should.equal false + }]; + this.resources = "mock-resources"; + this.OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, this.output_files); + this.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1); + return this.ResourceWriter._removeExtraneousFiles(this.resources, this.basePath, this.callback); + }); + + it("should find the existing output files", function() { + return this.OutputFileFinder.findOutputFiles + .calledWith(this.resources, this.basePath) + .should.equal(true); + }); + + it("should delete the output files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "output.pdf")) + .should.equal(true); + }); + + it("should delete the extra files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "extra/file.tex")) + .should.equal(true); + }); + + it("should not delete the extra aux files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "extra.aux")) + .should.equal(false); + }); - it "should not delete the knitr cache file", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "cache/_chunk1")) - .should.equal false - - it "should not delete the epstopdf converted files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "figures/image-eps-converted-to.pdf")) - .should.equal false - - it "should not delete the tikz md5 files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "foo/main-figure0.md5")) - .should.equal false - - it "should not delete the tikz dpth files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "foo/main-figure0.dpth")) - .should.equal false - - it "should not delete the tikz pdf files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "foo/main-figure0.pdf")) - .should.equal false - - it "should not delete the minted pygstyle files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "_minted-main/default-pyg-prefix.pygstyle")) - .should.equal false - - it "should not delete the minted default pygstyle files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "_minted-main/default.pygstyle")) - .should.equal false - - it "should not delete the minted default pygtex files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex")) - .should.equal false - - it "should not delete the markdown md.tex files", -> - @ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(@basePath, "_markdown_main/30893013dec5d869a415610079774c2f.md.tex")) - .should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - it "should time the request", -> - @Metrics.Timer::done.called.should.equal true - - describe "_writeResourceToDisk", -> - describe "with a url based resource", -> - beforeEach -> - @resource = - path: "main.tex" - url: "http://www.example.com/main.tex" + it("should not delete the knitr cache file", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "cache/_chunk1")) + .should.equal(false); + }); + + it("should not delete the epstopdf converted files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "figures/image-eps-converted-to.pdf")) + .should.equal(false); + }); + + it("should not delete the tikz md5 files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "foo/main-figure0.md5")) + .should.equal(false); + }); + + it("should not delete the tikz dpth files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "foo/main-figure0.dpth")) + .should.equal(false); + }); + + it("should not delete the tikz pdf files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "foo/main-figure0.pdf")) + .should.equal(false); + }); + + it("should not delete the minted pygstyle files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "_minted-main/default-pyg-prefix.pygstyle")) + .should.equal(false); + }); + + it("should not delete the minted default pygstyle files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "_minted-main/default.pygstyle")) + .should.equal(false); + }); + + it("should not delete the minted default pygtex files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex")) + .should.equal(false); + }); + + it("should not delete the markdown md.tex files", function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, "_markdown_main/30893013dec5d869a415610079774c2f.md.tex")) + .should.equal(false); + }); + + it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + + return it("should time the request", function() { + return this.Metrics.Timer.prototype.done.called.should.equal(true); + }); + }); + + describe("_writeResourceToDisk", function() { + describe("with a url based resource", function() { + beforeEach(function() { + this.resource = { + path: "main.tex", + url: "http://www.example.com/main.tex", modified: Date.now() - @UrlCache.downloadUrlToFile = sinon.stub().callsArgWith(4, "fake error downloading file") - @ResourceWriter._writeResourceToDisk(@project_id, @resource, @basePath, @callback) - - it "should ensure the directory exists", -> - @mkdirp - .calledWith(path.dirname(path.join(@basePath, @resource.path))) - .should.equal true - - it "should write the URL from the cache", -> - @UrlCache.downloadUrlToFile - .calledWith(@project_id, @resource.url, path.join(@basePath, @resource.path), @resource.modified) - .should.equal true + }; + this.UrlCache.downloadUrlToFile = sinon.stub().callsArgWith(4, "fake error downloading file"); + return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback); + }); + + it("should ensure the directory exists", function() { + return this.mkdirp + .calledWith(path.dirname(path.join(this.basePath, this.resource.path))) + .should.equal(true); + }); + + it("should write the URL from the cache", function() { + return this.UrlCache.downloadUrlToFile + .calledWith(this.project_id, this.resource.url, path.join(this.basePath, this.resource.path), this.resource.modified) + .should.equal(true); + }); - it "should call the callback", -> - @callback.called.should.equal true - - it "should not return an error if the resource writer errored", -> - should.not.exist @callback.args[0][0] - - describe "with a content based resource", -> - beforeEach -> - @resource = - path: "main.tex" + it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + + return it("should not return an error if the resource writer errored", function() { + return should.not.exist(this.callback.args[0][0]); + }); + }); + + describe("with a content based resource", function() { + beforeEach(function() { + this.resource = { + path: "main.tex", content: "Hello world" - @fs.writeFile = sinon.stub().callsArg(2) - @ResourceWriter._writeResourceToDisk(@project_id, @resource, @basePath, @callback) - - it "should ensure the directory exists", -> - @mkdirp - .calledWith(path.dirname(path.join(@basePath, @resource.path))) - .should.equal true - - it "should write the contents to disk", -> - @fs.writeFile - .calledWith(path.join(@basePath, @resource.path), @resource.content) - .should.equal true + }; + this.fs.writeFile = sinon.stub().callsArg(2); + return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback); + }); + + it("should ensure the directory exists", function() { + return this.mkdirp + .calledWith(path.dirname(path.join(this.basePath, this.resource.path))) + .should.equal(true); + }); + + it("should write the contents to disk", function() { + return this.fs.writeFile + .calledWith(path.join(this.basePath, this.resource.path), this.resource.content) + .should.equal(true); + }); - it "should call the callback", -> - @callback.called.should.equal true - - describe "with a file path that breaks out of the root folder", -> - beforeEach -> - @resource = - path: "../../main.tex" + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + return describe("with a file path that breaks out of the root folder", function() { + beforeEach(function() { + this.resource = { + path: "../../main.tex", content: "Hello world" - @fs.writeFile = sinon.stub().callsArg(2) - @ResourceWriter._writeResourceToDisk(@project_id, @resource, @basePath, @callback) + }; + this.fs.writeFile = sinon.stub().callsArg(2); + return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback); + }); - it "should not write to disk", -> - @fs.writeFile.called.should.equal false + it("should not write to disk", function() { + return this.fs.writeFile.called.should.equal(false); + }); - it "should return an error", -> - @callback + return it("should return an error", function() { + return this.callback .calledWith(new Error("resource path is outside root directory")) - .should.equal true + .should.equal(true); + }); + }); + }); - describe "checkPath", -> - describe "with a valid path", -> - beforeEach -> - @ResourceWriter.checkPath("foo", "bar", @callback) - - it "should return the joined path", -> - @callback.calledWith(null, "foo/bar") - .should.equal true - - describe "with an invalid path", -> - beforeEach -> - @ResourceWriter.checkPath("foo", "baz/../../bar", @callback) - - it "should return an error", -> - @callback.calledWith(new Error("resource path is outside root directory")) - .should.equal true - - describe "with another invalid path matching on a prefix", -> - beforeEach -> - @ResourceWriter.checkPath("foo", "../foobar/baz", @callback) - - it "should return an error", -> - @callback.calledWith(new Error("resource path is outside root directory")) - .should.equal true + return describe("checkPath", function() { + describe("with a valid path", function() { + beforeEach(function() { + return this.ResourceWriter.checkPath("foo", "bar", this.callback); + }); + + return it("should return the joined path", function() { + return this.callback.calledWith(null, "foo/bar") + .should.equal(true); + }); + }); + + describe("with an invalid path", function() { + beforeEach(function() { + return this.ResourceWriter.checkPath("foo", "baz/../../bar", this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith(new Error("resource path is outside root directory")) + .should.equal(true); + }); + }); + + return describe("with another invalid path matching on a prefix", function() { + beforeEach(function() { + return this.ResourceWriter.checkPath("foo", "../foobar/baz", this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWith(new Error("resource path is outside root directory")) + .should.equal(true); + }); + }); + }); +}); diff --git a/test/unit/coffee/StaticServerForbidSymlinksTests.js b/test/unit/coffee/StaticServerForbidSymlinksTests.js index 4a87d642..9063c1fd 100644 --- a/test/unit/coffee/StaticServerForbidSymlinksTests.js +++ b/test/unit/coffee/StaticServerForbidSymlinksTests.js @@ -1,158 +1,219 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../app/js/StaticServerForbidSymlinks" -expect = require("chai").expect - -describe "StaticServerForbidSymlinks", -> - - beforeEach -> - - @settings = - path: +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should(); +const SandboxedModule = require('sandboxed-module'); +const assert = require('assert'); +const path = require('path'); +const sinon = require('sinon'); +const modulePath = path.join(__dirname, "../../../app/js/StaticServerForbidSymlinks"); +const { expect } = require("chai"); + +describe("StaticServerForbidSymlinks", function() { + + beforeEach(function() { + + this.settings = { + path: { compilesDir: "/compiles/here" - - @fs = {} - @ForbidSymlinks = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": - log:-> - warn:-> - error:-> - "fs":@fs - - @dummyStatic = (rootDir, options) -> - return (req, res, next) -> - # console.log "dummyStatic serving file", rootDir, "called with", req.url - # serve it + } + }; + + this.fs = {}; + this.ForbidSymlinks = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex":this.settings, + "logger-sharelatex": { + log() {}, + warn() {}, + error() {} + }, + "fs":this.fs + } + } + ); + + this.dummyStatic = (rootDir, options) => + (req, res, next) => + // console.log "dummyStatic serving file", rootDir, "called with", req.url + // serve it next() + + ; - @StaticServerForbidSymlinks = @ForbidSymlinks @dummyStatic, @settings.path.compilesDir - @req = - params: + this.StaticServerForbidSymlinks = this.ForbidSymlinks(this.dummyStatic, this.settings.path.compilesDir); + this.req = { + params: { project_id:"12345" - - @res = {} - @req.url = "/12345/output.pdf" - - - describe "sending a normal file through", -> - beforeEach -> - @fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/#{@req.params.project_id}/output.pdf") - - it "should call next", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 200 - done() - @StaticServerForbidSymlinks @req, @res, done - - - describe "with a missing file", -> - beforeEach -> - @fs.realpath = sinon.stub().callsArgWith(1, {code: 'ENOENT'}, "#{@settings.path.compilesDir}/#{@req.params.project_id}/unknown.pdf") - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - - describe "with a symlink file", -> - beforeEach -> - @fs.realpath = sinon.stub().callsArgWith(1, null, "/etc/#{@req.params.project_id}/output.pdf") - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - - describe "with a relative file", -> - beforeEach -> - @req.url = "/12345/../67890/output.pdf" - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - - describe "with a unnormalized file containing .", -> - beforeEach -> - @req.url = "/12345/foo/./output.pdf" - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - - describe "with a file containing an empty path", -> - beforeEach -> - @req.url = "/12345/foo//output.pdf" - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - describe "with a non-project file", -> - beforeEach -> - @req.url = "/.foo/output.pdf" - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - describe "with a file outside the compiledir", -> - beforeEach -> - @req.url = "/../bar/output.pdf" - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - - describe "with a file with no leading /", -> - beforeEach -> - @req.url = "./../bar/output.pdf" - - it "should send a 404", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 404 - done() - @StaticServerForbidSymlinks @req, @res - - describe "with a github style path", -> - beforeEach -> - @req.url = "/henryoswald-latex_example/output/output.log" - @fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/henryoswald-latex_example/output/output.log") - - it "should call next", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 200 - done() - @StaticServerForbidSymlinks @req, @res, done - - describe "with an error from fs.realpath", -> - - beforeEach -> - @fs.realpath = sinon.stub().callsArgWith(1, "error") - - it "should send a 500", (done)-> - @res.sendStatus = (resCode)-> - resCode.should.equal 500 - done() - @StaticServerForbidSymlinks @req, @res + } + }; + + this.res = {}; + return this.req.url = "/12345/output.pdf"; + }); + + + describe("sending a normal file through", function() { + beforeEach(function() { + return this.fs.realpath = sinon.stub().callsArgWith(1, null, `${this.settings.path.compilesDir}/${this.req.params.project_id}/output.pdf`); + }); + + return it("should call next", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(200); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res, done); + }); + }); + + + describe("with a missing file", function() { + beforeEach(function() { + return this.fs.realpath = sinon.stub().callsArgWith(1, {code: 'ENOENT'}, `${this.settings.path.compilesDir}/${this.req.params.project_id}/unknown.pdf`); + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + + describe("with a symlink file", function() { + beforeEach(function() { + return this.fs.realpath = sinon.stub().callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`); + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + + describe("with a relative file", function() { + beforeEach(function() { + return this.req.url = "/12345/../67890/output.pdf"; + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + + describe("with a unnormalized file containing .", function() { + beforeEach(function() { + return this.req.url = "/12345/foo/./output.pdf"; + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + + describe("with a file containing an empty path", function() { + beforeEach(function() { + return this.req.url = "/12345/foo//output.pdf"; + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + describe("with a non-project file", function() { + beforeEach(function() { + return this.req.url = "/.foo/output.pdf"; + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + describe("with a file outside the compiledir", function() { + beforeEach(function() { + return this.req.url = "/../bar/output.pdf"; + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + + describe("with a file with no leading /", function() { + beforeEach(function() { + return this.req.url = "./../bar/output.pdf"; + }); + + return it("should send a 404", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(404); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); + + describe("with a github style path", function() { + beforeEach(function() { + this.req.url = "/henryoswald-latex_example/output/output.log"; + return this.fs.realpath = sinon.stub().callsArgWith(1, null, `${this.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`); + }); + + return it("should call next", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(200); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res, done); + }); + }); + + return describe("with an error from fs.realpath", function() { + + beforeEach(function() { + return this.fs.realpath = sinon.stub().callsArgWith(1, "error"); + }); + + return it("should send a 500", function(done){ + this.res.sendStatus = function(resCode){ + resCode.should.equal(500); + return done(); + }; + return this.StaticServerForbidSymlinks(this.req, this.res); + }); + }); +}); diff --git a/test/unit/coffee/TikzManager.js b/test/unit/coffee/TikzManager.js index 69968aa5..c792fabf 100644 --- a/test/unit/coffee/TikzManager.js +++ b/test/unit/coffee/TikzManager.js @@ -1,117 +1,150 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/TikzManager' - -describe 'TikzManager', -> - beforeEach -> - @TikzManager = SandboxedModule.require modulePath, requires: - "./ResourceWriter": @ResourceWriter = {} - "./SafeReader": @SafeReader = {} - "fs": @fs = {} - "logger-sharelatex": @logger = {log: () ->} - - describe "checkMainFile", -> - beforeEach -> - @compileDir = "compile-dir" - @mainFile = "main.tex" - @callback = sinon.stub() - - describe "if there is already an output.tex file in the resources", -> - beforeEach -> - @resources = [{path:"main.tex"},{path:"output.tex"}] - @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback - - it "should call the callback with false ", -> - @callback.calledWithExactly(null, false) - .should.equal true - - describe "if there is no output.tex file in the resources", -> - beforeEach -> - @resources = [{path:"main.tex"}] - @ResourceWriter.checkPath = sinon.stub() - .withArgs(@compileDir, @mainFile) - .callsArgWith(2, null, "#{@compileDir}/#{@mainFile}") - - describe "and the main file contains tikzexternalize", -> - beforeEach -> - @SafeReader.readFile = sinon.stub() - .withArgs("#{@compileDir}/#{@mainFile}") - .callsArgWith(3, null, "hello \\tikzexternalize") - @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback - - it "should look at the file on disk", -> - @SafeReader.readFile - .calledWith("#{@compileDir}/#{@mainFile}") - .should.equal true - - it "should call the callback with true ", -> - @callback.calledWithExactly(null, true) - .should.equal true - - describe "and the main file does not contain tikzexternalize", -> - beforeEach -> - @SafeReader.readFile = sinon.stub() - .withArgs("#{@compileDir}/#{@mainFile}") - .callsArgWith(3, null, "hello") - @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback - - it "should look at the file on disk", -> - @SafeReader.readFile - .calledWith("#{@compileDir}/#{@mainFile}") - .should.equal true - - it "should call the callback with false", -> - @callback.calledWithExactly(null, false) - .should.equal true - - describe "and the main file contains \\usepackage{pstool}", -> - beforeEach -> - @SafeReader.readFile = sinon.stub() - .withArgs("#{@compileDir}/#{@mainFile}") - .callsArgWith(3, null, "hello \\usepackage[random-options]{pstool}") - @TikzManager.checkMainFile @compileDir, @mainFile, @resources, @callback - - it "should look at the file on disk", -> - @SafeReader.readFile - .calledWith("#{@compileDir}/#{@mainFile}") - .should.equal true - - it "should call the callback with true ", -> - @callback.calledWithExactly(null, true) - .should.equal true - - describe "injectOutputFile", -> - beforeEach -> - @rootDir = "/mock" - @filename = "filename.tex" - @callback = sinon.stub() - @content = ''' - \\documentclass{article} - \\usepackage{tikz} - \\tikzexternalize - \\begin{document} - Hello world - \\end{document} - ''' - @fs.readFile = sinon.stub().callsArgWith(2, null, @content) - @fs.writeFile = sinon.stub().callsArg(3) - @ResourceWriter.checkPath = sinon.stub().callsArgWith(2, null, "#{@rootDir}/#{@filename}") - @TikzManager.injectOutputFile @rootDir, @filename, @callback - - it "sould check the path", -> - @ResourceWriter.checkPath.calledWith(@rootDir, @filename) - .should.equal true - - it "should read the file", -> - @fs.readFile - .calledWith("#{@rootDir}/#{@filename}", "utf8") - .should.equal true - - it "should write out the same file as output.tex", -> - @fs.writeFile - .calledWith("#{@rootDir}/output.tex", @content, {flag: 'wx'}) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/TikzManager'); + +describe('TikzManager', function() { + beforeEach(function() { + return this.TikzManager = SandboxedModule.require(modulePath, { requires: { + "./ResourceWriter": (this.ResourceWriter = {}), + "./SafeReader": (this.SafeReader = {}), + "fs": (this.fs = {}), + "logger-sharelatex": (this.logger = {log() {}}) + } + });}); + + describe("checkMainFile", function() { + beforeEach(function() { + this.compileDir = "compile-dir"; + this.mainFile = "main.tex"; + return this.callback = sinon.stub(); + }); + + describe("if there is already an output.tex file in the resources", function() { + beforeEach(function() { + this.resources = [{path:"main.tex"},{path:"output.tex"}]; + return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); + }); + + return it("should call the callback with false ", function() { + return this.callback.calledWithExactly(null, false) + .should.equal(true); + }); + }); + + return describe("if there is no output.tex file in the resources", function() { + beforeEach(function() { + this.resources = [{path:"main.tex"}]; + return this.ResourceWriter.checkPath = sinon.stub() + .withArgs(this.compileDir, this.mainFile) + .callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`); + }); + + describe("and the main file contains tikzexternalize", function() { + beforeEach(function() { + this.SafeReader.readFile = sinon.stub() + .withArgs(`${this.compileDir}/${this.mainFile}`) + .callsArgWith(3, null, "hello \\tikzexternalize"); + return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); + }); + + it("should look at the file on disk", function() { + return this.SafeReader.readFile + .calledWith(`${this.compileDir}/${this.mainFile}`) + .should.equal(true); + }); + + return it("should call the callback with true ", function() { + return this.callback.calledWithExactly(null, true) + .should.equal(true); + }); + }); + + describe("and the main file does not contain tikzexternalize", function() { + beforeEach(function() { + this.SafeReader.readFile = sinon.stub() + .withArgs(`${this.compileDir}/${this.mainFile}`) + .callsArgWith(3, null, "hello"); + return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); + }); + + it("should look at the file on disk", function() { + return this.SafeReader.readFile + .calledWith(`${this.compileDir}/${this.mainFile}`) + .should.equal(true); + }); + + return it("should call the callback with false", function() { + return this.callback.calledWithExactly(null, false) + .should.equal(true); + }); + }); + + return describe("and the main file contains \\usepackage{pstool}", function() { + beforeEach(function() { + this.SafeReader.readFile = sinon.stub() + .withArgs(`${this.compileDir}/${this.mainFile}`) + .callsArgWith(3, null, "hello \\usepackage[random-options]{pstool}"); + return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); + }); + + it("should look at the file on disk", function() { + return this.SafeReader.readFile + .calledWith(`${this.compileDir}/${this.mainFile}`) + .should.equal(true); + }); + + return it("should call the callback with true ", function() { + return this.callback.calledWithExactly(null, true) + .should.equal(true); + }); + }); + }); + }); + + return describe("injectOutputFile", function() { + beforeEach(function() { + this.rootDir = "/mock"; + this.filename = "filename.tex"; + this.callback = sinon.stub(); + this.content = `\ +\\documentclass{article} +\\usepackage{tikz} +\\tikzexternalize +\\begin{document} +Hello world +\\end{document}\ +`; + this.fs.readFile = sinon.stub().callsArgWith(2, null, this.content); + this.fs.writeFile = sinon.stub().callsArg(3); + this.ResourceWriter.checkPath = sinon.stub().callsArgWith(2, null, `${this.rootDir}/${this.filename}`); + return this.TikzManager.injectOutputFile(this.rootDir, this.filename, this.callback); + }); + + it("sould check the path", function() { + return this.ResourceWriter.checkPath.calledWith(this.rootDir, this.filename) + .should.equal(true); + }); + + it("should read the file", function() { + return this.fs.readFile + .calledWith(`${this.rootDir}/${this.filename}`, "utf8") + .should.equal(true); + }); + + it("should write out the same file as output.tex", function() { + return this.fs.writeFile + .calledWith(`${this.rootDir}/output.tex`, this.content, {flag: 'wx'}) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); +}); diff --git a/test/unit/coffee/UrlCacheTests.js b/test/unit/coffee/UrlCacheTests.js index 36a11cbb..a3af0081 100644 --- a/test/unit/coffee/UrlCacheTests.js +++ b/test/unit/coffee/UrlCacheTests.js @@ -1,200 +1,262 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/UrlCache' -EventEmitter = require("events").EventEmitter - -describe "UrlCache", -> - beforeEach -> - @callback = sinon.stub() - @url = "www.example.com/file" - @project_id = "project-id-123" - @UrlCache = SandboxedModule.require modulePath, requires: - "./db" : {} - "./UrlFetcher" : @UrlFetcher = {} - "logger-sharelatex": @logger = {log: sinon.stub()} - "settings-sharelatex": @Settings = { path: clsiCacheDir: "/cache/dir" } - "fs": @fs = {} +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/UrlCache'); +const { EventEmitter } = require("events"); + +describe("UrlCache", function() { + beforeEach(function() { + this.callback = sinon.stub(); + this.url = "www.example.com/file"; + this.project_id = "project-id-123"; + return this.UrlCache = SandboxedModule.require(modulePath, { requires: { + "./db" : {}, + "./UrlFetcher" : (this.UrlFetcher = {}), + "logger-sharelatex": (this.logger = {log: sinon.stub()}), + "settings-sharelatex": (this.Settings = { path: {clsiCacheDir: "/cache/dir"} }), + "fs": (this.fs = {}) + } + });}); - describe "_doesUrlNeedDownloading", -> - beforeEach -> - @lastModified = new Date() - @lastModifiedRoundedToSeconds = new Date(Math.floor(@lastModified.getTime() / 1000) * 1000) - - describe "when URL does not exist in cache", -> - beforeEach -> - @UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, null) - @UrlCache._doesUrlNeedDownloading(@project_id, @url, @lastModified, @callback) - - it "should return the callback with true", -> - @callback.calledWith(null, true).should.equal true - - describe "when URL does exist in cache", -> - beforeEach -> - @urlDetails = {} - @UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, @urlDetails) - - describe "when the modified date is more recent than the cached modified date", -> - beforeEach -> - @urlDetails.lastModified = new Date(@lastModified.getTime() - 1000) - @UrlCache._doesUrlNeedDownloading(@project_id, @url, @lastModified, @callback) - - it "should get the url details", -> - @UrlCache._findUrlDetails - .calledWith(@project_id, @url) - .should.equal true - - it "should return the callback with true", -> - @callback.calledWith(null, true).should.equal true - - describe "when the cached modified date is more recent than the modified date", -> - beforeEach -> - @urlDetails.lastModified = new Date(@lastModified.getTime() + 1000) - @UrlCache._doesUrlNeedDownloading(@project_id, @url, @lastModified, @callback) - - it "should return the callback with false", -> - @callback.calledWith(null, false).should.equal true - - describe "when the cached modified date is equal to the modified date", -> - beforeEach -> - @urlDetails.lastModified = @lastModified - @UrlCache._doesUrlNeedDownloading(@project_id, @url, @lastModified, @callback) - - it "should return the callback with false", -> - @callback.calledWith(null, false).should.equal true - - describe "when the provided modified date does not exist", -> - beforeEach -> - @lastModified = null - @UrlCache._doesUrlNeedDownloading(@project_id, @url, @lastModified, @callback) - - it "should return the callback with true", -> - @callback.calledWith(null, true).should.equal true - - describe "when the URL does not have a modified date", -> - beforeEach -> - @urlDetails.lastModified = null - @UrlCache._doesUrlNeedDownloading(@project_id, @url, @lastModified, @callback) - - it "should return the callback with true", -> - @callback.calledWith(null, true).should.equal true - - describe "_ensureUrlIsInCache", -> - beforeEach -> - @UrlFetcher.pipeUrlToFile = sinon.stub().callsArg(2) - @UrlCache._updateOrCreateUrlDetails = sinon.stub().callsArg(3) + describe("_doesUrlNeedDownloading", function() { + beforeEach(function() { + this.lastModified = new Date(); + return this.lastModifiedRoundedToSeconds = new Date(Math.floor(this.lastModified.getTime() / 1000) * 1000); + }); + + describe("when URL does not exist in cache", function() { + beforeEach(function() { + this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, null); + return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); + }); + + return it("should return the callback with true", function() { + return this.callback.calledWith(null, true).should.equal(true); + }); + }); + + return describe("when URL does exist in cache", function() { + beforeEach(function() { + this.urlDetails = {}; + return this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, this.urlDetails); + }); + + describe("when the modified date is more recent than the cached modified date", function() { + beforeEach(function() { + this.urlDetails.lastModified = new Date(this.lastModified.getTime() - 1000); + return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); + }); + + it("should get the url details", function() { + return this.UrlCache._findUrlDetails + .calledWith(this.project_id, this.url) + .should.equal(true); + }); + + return it("should return the callback with true", function() { + return this.callback.calledWith(null, true).should.equal(true); + }); + }); + + describe("when the cached modified date is more recent than the modified date", function() { + beforeEach(function() { + this.urlDetails.lastModified = new Date(this.lastModified.getTime() + 1000); + return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); + }); + + return it("should return the callback with false", function() { + return this.callback.calledWith(null, false).should.equal(true); + }); + }); + + describe("when the cached modified date is equal to the modified date", function() { + beforeEach(function() { + this.urlDetails.lastModified = this.lastModified; + return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); + }); + + return it("should return the callback with false", function() { + return this.callback.calledWith(null, false).should.equal(true); + }); + }); + + describe("when the provided modified date does not exist", function() { + beforeEach(function() { + this.lastModified = null; + return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); + }); + + return it("should return the callback with true", function() { + return this.callback.calledWith(null, true).should.equal(true); + }); + }); + + return describe("when the URL does not have a modified date", function() { + beforeEach(function() { + this.urlDetails.lastModified = null; + return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); + }); + + return it("should return the callback with true", function() { + return this.callback.calledWith(null, true).should.equal(true); + }); + }); + }); + }); + + describe("_ensureUrlIsInCache", function() { + beforeEach(function() { + this.UrlFetcher.pipeUrlToFile = sinon.stub().callsArg(2); + return this.UrlCache._updateOrCreateUrlDetails = sinon.stub().callsArg(3); + }); - describe "when the URL needs updating", -> - beforeEach -> - @UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, true) - @UrlCache._ensureUrlIsInCache(@project_id, @url, @lastModified, @callback) - - it "should check that the url needs downloading", -> - @UrlCache._doesUrlNeedDownloading - .calledWith(@project_id, @url, @lastModifiedRoundedToSeconds) - .should.equal true - - it "should download the URL to the cache file", -> - @UrlFetcher.pipeUrlToFile - .calledWith(@url, @UrlCache._cacheFilePathForUrl(@project_id, @url)) - .should.equal true + describe("when the URL needs updating", function() { + beforeEach(function() { + this.UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, true); + return this.UrlCache._ensureUrlIsInCache(this.project_id, this.url, this.lastModified, this.callback); + }); + + it("should check that the url needs downloading", function() { + return this.UrlCache._doesUrlNeedDownloading + .calledWith(this.project_id, this.url, this.lastModifiedRoundedToSeconds) + .should.equal(true); + }); + + it("should download the URL to the cache file", function() { + return this.UrlFetcher.pipeUrlToFile + .calledWith(this.url, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) + .should.equal(true); + }); - it "should update the database entry", -> - @UrlCache._updateOrCreateUrlDetails - .calledWith(@project_id, @url, @lastModifiedRoundedToSeconds) - .should.equal true - - it "should return the callback with the cache file path", -> - @callback - .calledWith(null, @UrlCache._cacheFilePathForUrl(@project_id, @url)) - .should.equal true - - describe "when the URL does not need updating", -> - beforeEach -> - @UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, false) - @UrlCache._ensureUrlIsInCache(@project_id, @url, @lastModified, @callback) + it("should update the database entry", function() { + return this.UrlCache._updateOrCreateUrlDetails + .calledWith(this.project_id, this.url, this.lastModifiedRoundedToSeconds) + .should.equal(true); + }); + + return it("should return the callback with the cache file path", function() { + return this.callback + .calledWith(null, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) + .should.equal(true); + }); + }); + + return describe("when the URL does not need updating", function() { + beforeEach(function() { + this.UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, false); + return this.UrlCache._ensureUrlIsInCache(this.project_id, this.url, this.lastModified, this.callback); + }); - it "should not download the URL to the cache file", -> - @UrlFetcher.pipeUrlToFile - .called.should.equal false - - it "should return the callback with the cache file path", -> - @callback - .calledWith(null, @UrlCache._cacheFilePathForUrl(@project_id, @url)) - .should.equal true - - describe "downloadUrlToFile", -> - beforeEach -> - @cachePath = "path/to/cached/url" - @destPath = "path/to/destination" - @UrlCache._copyFile = sinon.stub().callsArg(2) - @UrlCache._ensureUrlIsInCache = sinon.stub().callsArgWith(3, null, @cachePath) - @UrlCache.downloadUrlToFile(@project_id, @url, @destPath, @lastModified, @callback) - - it "should ensure the URL is downloaded and updated in the cache", -> - @UrlCache._ensureUrlIsInCache - .calledWith(@project_id, @url, @lastModified) - .should.equal true - - it "should copy the file to the new location", -> - @UrlCache._copyFile - .calledWith(@cachePath, @destPath) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "_deleteUrlCacheFromDisk", -> - beforeEach -> - @fs.unlink = sinon.stub().callsArg(1) - @UrlCache._deleteUrlCacheFromDisk(@project_id, @url, @callback) - - it "should delete the cache file", -> - @fs.unlink - .calledWith(@UrlCache._cacheFilePathForUrl(@project_id, @url)) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "_clearUrlFromCache", -> - beforeEach -> - @UrlCache._deleteUrlCacheFromDisk = sinon.stub().callsArg(2) - @UrlCache._clearUrlDetails = sinon.stub().callsArg(2) - @UrlCache._clearUrlFromCache @project_id, @url, @callback - - it "should delete the file on disk", -> - @UrlCache._deleteUrlCacheFromDisk - .calledWith(@project_id, @url) - .should.equal true - - it "should clear the entry in the database", -> - @UrlCache._clearUrlDetails - .calledWith(@project_id, @url) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "clearProject", -> - beforeEach -> - @urls = [ - "www.example.com/file1" + it("should not download the URL to the cache file", function() { + return this.UrlFetcher.pipeUrlToFile + .called.should.equal(false); + }); + + return it("should return the callback with the cache file path", function() { + return this.callback + .calledWith(null, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) + .should.equal(true); + }); + }); + }); + + describe("downloadUrlToFile", function() { + beforeEach(function() { + this.cachePath = "path/to/cached/url"; + this.destPath = "path/to/destination"; + this.UrlCache._copyFile = sinon.stub().callsArg(2); + this.UrlCache._ensureUrlIsInCache = sinon.stub().callsArgWith(3, null, this.cachePath); + return this.UrlCache.downloadUrlToFile(this.project_id, this.url, this.destPath, this.lastModified, this.callback); + }); + + it("should ensure the URL is downloaded and updated in the cache", function() { + return this.UrlCache._ensureUrlIsInCache + .calledWith(this.project_id, this.url, this.lastModified) + .should.equal(true); + }); + + it("should copy the file to the new location", function() { + return this.UrlCache._copyFile + .calledWith(this.cachePath, this.destPath) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + describe("_deleteUrlCacheFromDisk", function() { + beforeEach(function() { + this.fs.unlink = sinon.stub().callsArg(1); + return this.UrlCache._deleteUrlCacheFromDisk(this.project_id, this.url, this.callback); + }); + + it("should delete the cache file", function() { + return this.fs.unlink + .calledWith(this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + describe("_clearUrlFromCache", function() { + beforeEach(function() { + this.UrlCache._deleteUrlCacheFromDisk = sinon.stub().callsArg(2); + this.UrlCache._clearUrlDetails = sinon.stub().callsArg(2); + return this.UrlCache._clearUrlFromCache(this.project_id, this.url, this.callback); + }); + + it("should delete the file on disk", function() { + return this.UrlCache._deleteUrlCacheFromDisk + .calledWith(this.project_id, this.url) + .should.equal(true); + }); + + it("should clear the entry in the database", function() { + return this.UrlCache._clearUrlDetails + .calledWith(this.project_id, this.url) + .should.equal(true); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + return describe("clearProject", function() { + beforeEach(function() { + this.urls = [ + "www.example.com/file1", "www.example.com/file2" - ] - @UrlCache._findAllUrlsInProject = sinon.stub().callsArgWith(1, null, @urls) - @UrlCache._clearUrlFromCache = sinon.stub().callsArg(2) - @UrlCache.clearProject @project_id, @callback - - it "should clear the cache for each url in the project", -> - for url in @urls - @UrlCache._clearUrlFromCache - .calledWith(@project_id, url) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true + ]; + this.UrlCache._findAllUrlsInProject = sinon.stub().callsArgWith(1, null, this.urls); + this.UrlCache._clearUrlFromCache = sinon.stub().callsArg(2); + return this.UrlCache.clearProject(this.project_id, this.callback); + }); + + it("should clear the cache for each url in the project", function() { + return Array.from(this.urls).map((url) => + this.UrlCache._clearUrlFromCache + .calledWith(this.project_id, url) + .should.equal(true)); + }); + + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); +}); diff --git a/test/unit/coffee/UrlFetcherTests.js b/test/unit/coffee/UrlFetcherTests.js index e91720e5..21258ab8 100644 --- a/test/unit/coffee/UrlFetcherTests.js +++ b/test/unit/coffee/UrlFetcherTests.js @@ -1,120 +1,154 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/UrlFetcher' -EventEmitter = require("events").EventEmitter - -describe "UrlFetcher", -> - beforeEach -> - @callback = sinon.stub() - @url = "https://www.example.com/file/here?query=string" - @UrlFetcher = SandboxedModule.require modulePath, requires: - request: defaults: @defaults = sinon.stub().returns(@request = {}) - fs: @fs = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } - "settings-sharelatex": @settings = {} - - it "should turn off the cookie jar in request", -> - @defaults.calledWith(jar: false) - .should.equal true - - describe "rewrite url domain if filestoreDomainOveride is set", -> - beforeEach -> - @path = "/path/to/file/on/disk" - @request.get = sinon.stub().returns(@urlStream = new EventEmitter) - @urlStream.pipe = sinon.stub() - @urlStream.pause = sinon.stub() - @urlStream.resume = sinon.stub() - @fs.createWriteStream = sinon.stub().returns(@fileStream = new EventEmitter) - @fs.unlink = (file, callback) -> callback() - - it "should use the normal domain when override not set", (done)-> - @UrlFetcher.pipeUrlToFile @url, @path, => - @request.get.args[0][0].url.should.equal @url - done() - @res = statusCode: 200 - @urlStream.emit "response", @res - @urlStream.emit "end" - @fileStream.emit "finish" - - - it "should use override domain when filestoreDomainOveride is set", (done)-> - @settings.filestoreDomainOveride = "192.11.11.11" - @UrlFetcher.pipeUrlToFile @url, @path, => - @request.get.args[0][0].url.should.equal "192.11.11.11/file/here?query=string" - done() - @res = statusCode: 200 - @urlStream.emit "response", @res - @urlStream.emit "end" - @fileStream.emit "finish" - - describe "pipeUrlToFile", -> - beforeEach (done)-> - @path = "/path/to/file/on/disk" - @request.get = sinon.stub().returns(@urlStream = new EventEmitter) - @urlStream.pipe = sinon.stub() - @urlStream.pause = sinon.stub() - @urlStream.resume = sinon.stub() - @fs.createWriteStream = sinon.stub().returns(@fileStream = new EventEmitter) - @fs.unlink = (file, callback) -> callback() - done() - - describe "successfully", -> - beforeEach (done)-> - @UrlFetcher.pipeUrlToFile @url, @path, => - @callback() - done() - @res = statusCode: 200 - @urlStream.emit "response", @res - @urlStream.emit "end" - @fileStream.emit "finish" - - - it "should request the URL", -> - @request.get - .calledWith(sinon.match {"url": @url}) - .should.equal true - - it "should open the file for writing", -> - @fs.createWriteStream - .calledWith(@path) - .should.equal true - - it "should pipe the URL to the file", -> - @urlStream.pipe - .calledWith(@fileStream) - .should.equal true +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/UrlFetcher'); +const { EventEmitter } = require("events"); + +describe("UrlFetcher", function() { + beforeEach(function() { + this.callback = sinon.stub(); + this.url = "https://www.example.com/file/here?query=string"; + return this.UrlFetcher = SandboxedModule.require(modulePath, { requires: { + request: { defaults: (this.defaults = sinon.stub().returns(this.request = {})) + }, + fs: (this.fs = {}), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), + "settings-sharelatex": (this.settings = {}) + } + });}); + + it("should turn off the cookie jar in request", function() { + return this.defaults.calledWith({jar: false}) + .should.equal(true); + }); + + describe("rewrite url domain if filestoreDomainOveride is set", function() { + beforeEach(function() { + this.path = "/path/to/file/on/disk"; + this.request.get = sinon.stub().returns(this.urlStream = new EventEmitter); + this.urlStream.pipe = sinon.stub(); + this.urlStream.pause = sinon.stub(); + this.urlStream.resume = sinon.stub(); + this.fs.createWriteStream = sinon.stub().returns(this.fileStream = new EventEmitter); + return this.fs.unlink = (file, callback) => callback(); + }); + + it("should use the normal domain when override not set", function(done){ + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.request.get.args[0][0].url.should.equal(this.url); + return done(); + }); + this.res = {statusCode: 200}; + this.urlStream.emit("response", this.res); + this.urlStream.emit("end"); + return this.fileStream.emit("finish"); + }); + + + return it("should use override domain when filestoreDomainOveride is set", function(done){ + this.settings.filestoreDomainOveride = "192.11.11.11"; + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.request.get.args[0][0].url.should.equal("192.11.11.11/file/here?query=string"); + return done(); + }); + this.res = {statusCode: 200}; + this.urlStream.emit("response", this.res); + this.urlStream.emit("end"); + return this.fileStream.emit("finish"); + }); + }); + + return describe("pipeUrlToFile", function() { + beforeEach(function(done){ + this.path = "/path/to/file/on/disk"; + this.request.get = sinon.stub().returns(this.urlStream = new EventEmitter); + this.urlStream.pipe = sinon.stub(); + this.urlStream.pause = sinon.stub(); + this.urlStream.resume = sinon.stub(); + this.fs.createWriteStream = sinon.stub().returns(this.fileStream = new EventEmitter); + this.fs.unlink = (file, callback) => callback(); + return done(); + }); + + describe("successfully", function() { + beforeEach(function(done){ + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.callback(); + return done(); + }); + this.res = {statusCode: 200}; + this.urlStream.emit("response", this.res); + this.urlStream.emit("end"); + return this.fileStream.emit("finish"); + }); + + + it("should request the URL", function() { + return this.request.get + .calledWith(sinon.match({"url": this.url})) + .should.equal(true); + }); + + it("should open the file for writing", function() { + return this.fs.createWriteStream + .calledWith(this.path) + .should.equal(true); + }); + + it("should pipe the URL to the file", function() { + return this.urlStream.pipe + .calledWith(this.fileStream) + .should.equal(true); + }); - it "should call the callback", -> - @callback.called.should.equal true - - describe "with non success status code", -> - beforeEach (done)-> - @UrlFetcher.pipeUrlToFile @url, @path, (err)=> - @callback(err) - done() - @res = statusCode: 404 - @urlStream.emit "response", @res - @urlStream.emit "end" - - it "should call the callback with an error", -> - @callback + return it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + }); + + describe("with non success status code", function() { + beforeEach(function(done){ + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err=> { + this.callback(err); + return done(); + }); + this.res = {statusCode: 404}; + this.urlStream.emit("response", this.res); + return this.urlStream.emit("end"); + }); + + return it("should call the callback with an error", function() { + return this.callback .calledWith(new Error("URL returned non-success status code: 404")) - .should.equal true - - describe "with error", -> - beforeEach (done)-> - @UrlFetcher.pipeUrlToFile @url, @path, (err)=> - @callback(err) - done() - @urlStream.emit "error", @error = new Error("something went wrong") - - it "should call the callback with the error", -> - @callback - .calledWith(@error) - .should.equal true - - it "should only call the callback once, even if end is called", -> - @urlStream.emit "end" - @callback.calledOnce.should.equal true + .should.equal(true); + }); + }); + + return describe("with error", function() { + beforeEach(function(done){ + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err=> { + this.callback(err); + return done(); + }); + return this.urlStream.emit("error", (this.error = new Error("something went wrong"))); + }); + + it("should call the callback with the error", function() { + return this.callback + .calledWith(this.error) + .should.equal(true); + }); + + return it("should only call the callback once, even if end is called", function() { + this.urlStream.emit("end"); + return this.callback.calledOnce.should.equal(true); + }); + }); + }); +}); From 0cb5426548ceac3e95d74c7c5afedcfa7600dd30 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:15:25 +0100 Subject: [PATCH 477/709] decaffeinate: Run post-processing cleanups on CompileControllerTests.coffee and 17 other files --- test/unit/coffee/CompileControllerTests.js | 6 ++++++ test/unit/coffee/CompileManagerTests.js | 9 +++++++++ test/unit/coffee/ContentTypeMapperTests.js | 7 +++++++ test/unit/coffee/DockerLockManagerTests.js | 5 +++++ test/unit/coffee/DockerRunnerTests.js | 9 ++++++++- test/unit/coffee/DraftModeManagerTests.js | 5 +++++ test/unit/coffee/LatexRunnerTests.js | 6 ++++++ test/unit/coffee/LockManagerTests.js | 6 ++++++ test/unit/coffee/OutputFileFinderTests.js | 7 +++++++ test/unit/coffee/OutputFileOptimiserTests.js | 7 +++++++ test/unit/coffee/ProjectPersistenceManagerTests.js | 7 +++++++ test/unit/coffee/RequestParserTests.js | 8 +++++++- test/unit/coffee/ResourceStateManagerTests.js | 6 ++++++ test/unit/coffee/ResourceWriterTests.js | 5 +++++ test/unit/coffee/StaticServerForbidSymlinksTests.js | 6 ++++++ test/unit/coffee/TikzManager.js | 5 +++++ test/unit/coffee/UrlCacheTests.js | 6 ++++++ test/unit/coffee/UrlFetcherTests.js | 5 +++++ 18 files changed, 113 insertions(+), 2 deletions(-) diff --git a/test/unit/coffee/CompileControllerTests.js b/test/unit/coffee/CompileControllerTests.js index 1defed70..2a06fbc3 100644 --- a/test/unit/coffee/CompileControllerTests.js +++ b/test/unit/coffee/CompileControllerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/CompileManagerTests.js b/test/unit/coffee/CompileManagerTests.js index 5675ac15..e798aec3 100644 --- a/test/unit/coffee/CompileManagerTests.js +++ b/test/unit/coffee/CompileManagerTests.js @@ -1,3 +1,12 @@ +/* eslint-disable + camelcase, + chai-friendly/no-unused-expressions, + no-path-concat, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/test/unit/coffee/ContentTypeMapperTests.js b/test/unit/coffee/ContentTypeMapperTests.js index 64a60914..bbde2923 100644 --- a/test/unit/coffee/ContentTypeMapperTests.js +++ b/test/unit/coffee/ContentTypeMapperTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/DockerLockManagerTests.js b/test/unit/coffee/DockerLockManagerTests.js index 5ef3ca2c..155a2464 100644 --- a/test/unit/coffee/DockerLockManagerTests.js +++ b/test/unit/coffee/DockerLockManagerTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/test/unit/coffee/DockerRunnerTests.js b/test/unit/coffee/DockerRunnerTests.js index 79ac5df1..152b8b96 100644 --- a/test/unit/coffee/DockerRunnerTests.js +++ b/test/unit/coffee/DockerRunnerTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -247,7 +254,7 @@ describe("DockerRunner", function() { this.container.inspect = sinon.stub().callsArgWith(0); this.container.start = sinon.stub().yields(); - return this.DockerRunner.startContainer(this.options, this.volumes, this.callback, function() {}); + return this.DockerRunner.startContainer(this.options, this.volumes, this.callback, () => {}); }); it("should start the container with the given name", function() { diff --git a/test/unit/coffee/DraftModeManagerTests.js b/test/unit/coffee/DraftModeManagerTests.js index ffea0508..f270873a 100644 --- a/test/unit/coffee/DraftModeManagerTests.js +++ b/test/unit/coffee/DraftModeManagerTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/LatexRunnerTests.js b/test/unit/coffee/LatexRunnerTests.js index 5cb4d066..7fe8bc81 100644 --- a/test/unit/coffee/LatexRunnerTests.js +++ b/test/unit/coffee/LatexRunnerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/LockManagerTests.js b/test/unit/coffee/LockManagerTests.js index d716a443..6d1b1562 100644 --- a/test/unit/coffee/LockManagerTests.js +++ b/test/unit/coffee/LockManagerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/OutputFileFinderTests.js b/test/unit/coffee/OutputFileFinderTests.js index 3292d0a3..5c956adb 100644 --- a/test/unit/coffee/OutputFileFinderTests.js +++ b/test/unit/coffee/OutputFileFinderTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/OutputFileOptimiserTests.js b/test/unit/coffee/OutputFileOptimiserTests.js index 8934c717..13b8d60c 100644 --- a/test/unit/coffee/OutputFileOptimiserTests.js +++ b/test/unit/coffee/OutputFileOptimiserTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/ProjectPersistenceManagerTests.js b/test/unit/coffee/ProjectPersistenceManagerTests.js index c15cd808..5f77a807 100644 --- a/test/unit/coffee/ProjectPersistenceManagerTests.js +++ b/test/unit/coffee/ProjectPersistenceManagerTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/test/unit/coffee/RequestParserTests.js b/test/unit/coffee/RequestParserTests.js index 5ca09411..725988f6 100644 --- a/test/unit/coffee/RequestParserTests.js +++ b/test/unit/coffee/RequestParserTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -35,7 +41,7 @@ describe("RequestParser", function() { } });}); - afterEach(() => tk.reset()); + afterEach(function() { return tk.reset(); }); describe("without a top level object", function() { beforeEach(function() { diff --git a/test/unit/coffee/ResourceStateManagerTests.js b/test/unit/coffee/ResourceStateManagerTests.js index 4b09135e..fe52cc58 100644 --- a/test/unit/coffee/ResourceStateManagerTests.js +++ b/test/unit/coffee/ResourceStateManagerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/ResourceWriterTests.js b/test/unit/coffee/ResourceWriterTests.js index 89433c82..83095473 100644 --- a/test/unit/coffee/ResourceWriterTests.js +++ b/test/unit/coffee/ResourceWriterTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/test/unit/coffee/StaticServerForbidSymlinksTests.js b/test/unit/coffee/StaticServerForbidSymlinksTests.js index 9063c1fd..e754ea75 100644 --- a/test/unit/coffee/StaticServerForbidSymlinksTests.js +++ b/test/unit/coffee/StaticServerForbidSymlinksTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/TikzManager.js b/test/unit/coffee/TikzManager.js index c792fabf..f35d2619 100644 --- a/test/unit/coffee/TikzManager.js +++ b/test/unit/coffee/TikzManager.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/unit/coffee/UrlCacheTests.js b/test/unit/coffee/UrlCacheTests.js index a3af0081..7f024507 100644 --- a/test/unit/coffee/UrlCacheTests.js +++ b/test/unit/coffee/UrlCacheTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/test/unit/coffee/UrlFetcherTests.js b/test/unit/coffee/UrlFetcherTests.js index 21258ab8..453a3867 100644 --- a/test/unit/coffee/UrlFetcherTests.js +++ b/test/unit/coffee/UrlFetcherTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns From b515397b5af58153c944c405d376413e95da1d0d Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:15:30 +0100 Subject: [PATCH 478/709] decaffeinate: rename test/unit/coffee to test/unit/js --- test/unit/{coffee => js}/CompileControllerTests.js | 0 test/unit/{coffee => js}/CompileManagerTests.js | 0 test/unit/{coffee => js}/ContentTypeMapperTests.js | 0 test/unit/{coffee => js}/DockerLockManagerTests.js | 0 test/unit/{coffee => js}/DockerRunnerTests.js | 0 test/unit/{coffee => js}/DraftModeManagerTests.js | 0 test/unit/{coffee => js}/LatexRunnerTests.js | 0 test/unit/{coffee => js}/LockManagerTests.js | 0 test/unit/{coffee => js}/OutputFileFinderTests.js | 0 test/unit/{coffee => js}/OutputFileOptimiserTests.js | 0 test/unit/{coffee => js}/ProjectPersistenceManagerTests.js | 0 test/unit/{coffee => js}/RequestParserTests.js | 0 test/unit/{coffee => js}/ResourceStateManagerTests.js | 0 test/unit/{coffee => js}/ResourceWriterTests.js | 0 test/unit/{coffee => js}/StaticServerForbidSymlinksTests.js | 0 test/unit/{coffee => js}/TikzManager.js | 0 test/unit/{coffee => js}/UrlCacheTests.js | 0 test/unit/{coffee => js}/UrlFetcherTests.js | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename test/unit/{coffee => js}/CompileControllerTests.js (100%) rename test/unit/{coffee => js}/CompileManagerTests.js (100%) rename test/unit/{coffee => js}/ContentTypeMapperTests.js (100%) rename test/unit/{coffee => js}/DockerLockManagerTests.js (100%) rename test/unit/{coffee => js}/DockerRunnerTests.js (100%) rename test/unit/{coffee => js}/DraftModeManagerTests.js (100%) rename test/unit/{coffee => js}/LatexRunnerTests.js (100%) rename test/unit/{coffee => js}/LockManagerTests.js (100%) rename test/unit/{coffee => js}/OutputFileFinderTests.js (100%) rename test/unit/{coffee => js}/OutputFileOptimiserTests.js (100%) rename test/unit/{coffee => js}/ProjectPersistenceManagerTests.js (100%) rename test/unit/{coffee => js}/RequestParserTests.js (100%) rename test/unit/{coffee => js}/ResourceStateManagerTests.js (100%) rename test/unit/{coffee => js}/ResourceWriterTests.js (100%) rename test/unit/{coffee => js}/StaticServerForbidSymlinksTests.js (100%) rename test/unit/{coffee => js}/TikzManager.js (100%) rename test/unit/{coffee => js}/UrlCacheTests.js (100%) rename test/unit/{coffee => js}/UrlFetcherTests.js (100%) diff --git a/test/unit/coffee/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js similarity index 100% rename from test/unit/coffee/CompileControllerTests.js rename to test/unit/js/CompileControllerTests.js diff --git a/test/unit/coffee/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js similarity index 100% rename from test/unit/coffee/CompileManagerTests.js rename to test/unit/js/CompileManagerTests.js diff --git a/test/unit/coffee/ContentTypeMapperTests.js b/test/unit/js/ContentTypeMapperTests.js similarity index 100% rename from test/unit/coffee/ContentTypeMapperTests.js rename to test/unit/js/ContentTypeMapperTests.js diff --git a/test/unit/coffee/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js similarity index 100% rename from test/unit/coffee/DockerLockManagerTests.js rename to test/unit/js/DockerLockManagerTests.js diff --git a/test/unit/coffee/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js similarity index 100% rename from test/unit/coffee/DockerRunnerTests.js rename to test/unit/js/DockerRunnerTests.js diff --git a/test/unit/coffee/DraftModeManagerTests.js b/test/unit/js/DraftModeManagerTests.js similarity index 100% rename from test/unit/coffee/DraftModeManagerTests.js rename to test/unit/js/DraftModeManagerTests.js diff --git a/test/unit/coffee/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js similarity index 100% rename from test/unit/coffee/LatexRunnerTests.js rename to test/unit/js/LatexRunnerTests.js diff --git a/test/unit/coffee/LockManagerTests.js b/test/unit/js/LockManagerTests.js similarity index 100% rename from test/unit/coffee/LockManagerTests.js rename to test/unit/js/LockManagerTests.js diff --git a/test/unit/coffee/OutputFileFinderTests.js b/test/unit/js/OutputFileFinderTests.js similarity index 100% rename from test/unit/coffee/OutputFileFinderTests.js rename to test/unit/js/OutputFileFinderTests.js diff --git a/test/unit/coffee/OutputFileOptimiserTests.js b/test/unit/js/OutputFileOptimiserTests.js similarity index 100% rename from test/unit/coffee/OutputFileOptimiserTests.js rename to test/unit/js/OutputFileOptimiserTests.js diff --git a/test/unit/coffee/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js similarity index 100% rename from test/unit/coffee/ProjectPersistenceManagerTests.js rename to test/unit/js/ProjectPersistenceManagerTests.js diff --git a/test/unit/coffee/RequestParserTests.js b/test/unit/js/RequestParserTests.js similarity index 100% rename from test/unit/coffee/RequestParserTests.js rename to test/unit/js/RequestParserTests.js diff --git a/test/unit/coffee/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js similarity index 100% rename from test/unit/coffee/ResourceStateManagerTests.js rename to test/unit/js/ResourceStateManagerTests.js diff --git a/test/unit/coffee/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js similarity index 100% rename from test/unit/coffee/ResourceWriterTests.js rename to test/unit/js/ResourceWriterTests.js diff --git a/test/unit/coffee/StaticServerForbidSymlinksTests.js b/test/unit/js/StaticServerForbidSymlinksTests.js similarity index 100% rename from test/unit/coffee/StaticServerForbidSymlinksTests.js rename to test/unit/js/StaticServerForbidSymlinksTests.js diff --git a/test/unit/coffee/TikzManager.js b/test/unit/js/TikzManager.js similarity index 100% rename from test/unit/coffee/TikzManager.js rename to test/unit/js/TikzManager.js diff --git a/test/unit/coffee/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js similarity index 100% rename from test/unit/coffee/UrlCacheTests.js rename to test/unit/js/UrlCacheTests.js diff --git a/test/unit/coffee/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js similarity index 100% rename from test/unit/coffee/UrlFetcherTests.js rename to test/unit/js/UrlFetcherTests.js From 7e2542319fe91ca0a92e2e1b860fe1503c8e643c Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:15:37 +0100 Subject: [PATCH 479/709] prettier: convert test/unit decaffeinated files to Prettier format --- test/unit/js/CompileControllerTests.js | 518 +++--- test/unit/js/CompileManagerTests.js | 956 ++++++----- test/unit/js/ContentTypeMapperTests.js | 115 +- test/unit/js/DockerLockManagerTests.js | 423 +++-- test/unit/js/DockerRunnerTests.js | 1471 +++++++++-------- test/unit/js/DraftModeManagerTests.js | 118 +- test/unit/js/LatexRunnerTests.js | 208 ++- test/unit/js/LockManagerTests.js | 138 +- test/unit/js/OutputFileFinderTests.js | 172 +- test/unit/js/OutputFileOptimiserTests.js | 320 ++-- .../unit/js/ProjectPersistenceManagerTests.js | 145 +- test/unit/js/RequestParserTests.js | 784 ++++----- test/unit/js/ResourceStateManagerTests.js | 339 ++-- test/unit/js/ResourceWriterTests.js | 890 +++++----- .../js/StaticServerForbidSymlinksTests.js | 440 ++--- test/unit/js/TikzManager.js | 306 ++-- test/unit/js/UrlCacheTests.js | 600 ++++--- test/unit/js/UrlFetcherTests.js | 311 ++-- 18 files changed, 4530 insertions(+), 3724 deletions(-) diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 2a06fbc3..4480c880 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -9,267 +9,299 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/CompileController'); -const tk = require("timekeeper"); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/CompileController' +) +const tk = require('timekeeper') -describe("CompileController", function() { - beforeEach(function() { - this.CompileController = SandboxedModule.require(modulePath, { requires: { - "./CompileManager": (this.CompileManager = {}), - "./RequestParser": (this.RequestParser = {}), - "settings-sharelatex": (this.Settings = { - apis: { - clsi: { - url: "http://clsi.example.com" - } - } - }), - "./ProjectPersistenceManager": (this.ProjectPersistenceManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), err:sinon.stub(), warn: sinon.stub()}) - } - }); - this.Settings.externalUrl = "http://www.example.com"; - this.req = {}; - this.res = {}; - return this.next = sinon.stub(); - }); +describe('CompileController', function() { + beforeEach(function() { + this.CompileController = SandboxedModule.require(modulePath, { + requires: { + './CompileManager': (this.CompileManager = {}), + './RequestParser': (this.RequestParser = {}), + 'settings-sharelatex': (this.Settings = { + apis: { + clsi: { + url: 'http://clsi.example.com' + } + } + }), + './ProjectPersistenceManager': (this.ProjectPersistenceManager = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err: sinon.stub(), + warn: sinon.stub() + }) + } + }) + this.Settings.externalUrl = 'http://www.example.com' + this.req = {} + this.res = {} + return (this.next = sinon.stub()) + }) - describe("compile", function() { - beforeEach(function() { - this.req.body = { - compile: "mock-body" - }; - this.req.params = - {project_id: (this.project_id = "project-id-123")}; - this.request = { - compile: "mock-parsed-request" - }; - this.request_with_project_id = { - compile: this.request.compile, - project_id: this.project_id - }; - this.output_files = [{ - path: "output.pdf", - type: "pdf", - build: 1234 - }, { - path: "output.log", - type: "log", - build: 1234 - }]; - this.RequestParser.parse = sinon.stub().callsArgWith(1, null, this.request); - this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon.stub().callsArg(1); - this.res.status = sinon.stub().returnsThis(); - return this.res.send = sinon.stub(); - }); + describe('compile', function() { + beforeEach(function() { + this.req.body = { + compile: 'mock-body' + } + this.req.params = { project_id: (this.project_id = 'project-id-123') } + this.request = { + compile: 'mock-parsed-request' + } + this.request_with_project_id = { + compile: this.request.compile, + project_id: this.project_id + } + this.output_files = [ + { + path: 'output.pdf', + type: 'pdf', + build: 1234 + }, + { + path: 'output.log', + type: 'log', + build: 1234 + } + ] + this.RequestParser.parse = sinon + .stub() + .callsArgWith(1, null, this.request) + this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon + .stub() + .callsArg(1) + this.res.status = sinon.stub().returnsThis() + return (this.res.send = sinon.stub()) + }) - describe("successfully", function() { - beforeEach(function() { - this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, this.output_files); - return this.CompileController.compile(this.req, this.res); - }); + describe('successfully', function() { + beforeEach(function() { + this.CompileManager.doCompileWithLock = sinon + .stub() + .callsArgWith(1, null, this.output_files) + return this.CompileController.compile(this.req, this.res) + }) - it("should parse the request", function() { - return this.RequestParser.parse - .calledWith(this.req.body) - .should.equal(true); - }); + it('should parse the request', function() { + return this.RequestParser.parse + .calledWith(this.req.body) + .should.equal(true) + }) - it("should run the compile for the specified project", function() { - return this.CompileManager.doCompileWithLock - .calledWith(this.request_with_project_id) - .should.equal(true); - }); + it('should run the compile for the specified project', function() { + return this.CompileManager.doCompileWithLock + .calledWith(this.request_with_project_id) + .should.equal(true) + }) - it("should mark the project as accessed", function() { - return this.ProjectPersistenceManager.markProjectAsJustAccessed - .calledWith(this.project_id) - .should.equal(true); - }); + it('should mark the project as accessed', function() { + return this.ProjectPersistenceManager.markProjectAsJustAccessed + .calledWith(this.project_id) + .should.equal(true) + }) - return it("should return the JSON response", function() { - this.res.status.calledWith(200).should.equal(true); - return this.res.send - .calledWith({ - compile: { - status: "success", - error: null, - outputFiles: this.output_files.map(file => { - return { - url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, - path: file.path, - type: file.type, - build: file.build - }; - }) - } - }) - .should.equal(true); - }); - }); - - describe("with an error", function() { - beforeEach(function() { - this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, new Error(this.message = "error message"), null); - return this.CompileController.compile(this.req, this.res); - }); - - return it("should return the JSON response with the error", function() { - this.res.status.calledWith(500).should.equal(true); - return this.res.send - .calledWith({ - compile: { - status: "error", - error: this.message, - outputFiles: [] - } - }) - .should.equal(true); - }); - }); + return it('should return the JSON response', function() { + this.res.status.calledWith(200).should.equal(true) + return this.res.send + .calledWith({ + compile: { + status: 'success', + error: null, + outputFiles: this.output_files.map(file => { + return { + url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, + path: file.path, + type: file.type, + build: file.build + } + }) + } + }) + .should.equal(true) + }) + }) - describe("when the request times out", function() { - beforeEach(function() { - this.error = new Error(this.message = "container timed out"); - this.error.timedout = true; - this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, this.error, null); - return this.CompileController.compile(this.req, this.res); - }); - - return it("should return the JSON response with the timeout status", function() { - this.res.status.calledWith(200).should.equal(true); - return this.res.send - .calledWith({ - compile: { - status: "timedout", - error: this.message, - outputFiles: [] - } - }) - .should.equal(true); - }); - }); + describe('with an error', function() { + beforeEach(function() { + this.CompileManager.doCompileWithLock = sinon + .stub() + .callsArgWith(1, new Error((this.message = 'error message')), null) + return this.CompileController.compile(this.req, this.res) + }) - return describe("when the request returns no output files", function() { - beforeEach(function() { - this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, []); - return this.CompileController.compile(this.req, this.res); - }); - - return it("should return the JSON response with the failure status", function() { - this.res.status.calledWith(200).should.equal(true); - return this.res.send - .calledWith({ - compile: { - error: null, - status: "failure", - outputFiles: [] - } - }) - .should.equal(true); - }); - }); - }); + return it('should return the JSON response with the error', function() { + this.res.status.calledWith(500).should.equal(true) + return this.res.send + .calledWith({ + compile: { + status: 'error', + error: this.message, + outputFiles: [] + } + }) + .should.equal(true) + }) + }) - describe("syncFromCode", function() { - beforeEach(function() { - this.file = "main.tex"; - this.line = 42; - this.column = 5; - this.project_id = "mock-project-id"; - this.req.params = - {project_id: this.project_id}; - this.req.query = { - file: this.file, - line: this.line.toString(), - column: this.column.toString() - }; - this.res.json = sinon.stub(); + describe('when the request times out', function() { + beforeEach(function() { + this.error = new Error((this.message = 'container timed out')) + this.error.timedout = true + this.CompileManager.doCompileWithLock = sinon + .stub() + .callsArgWith(1, this.error, null) + return this.CompileController.compile(this.req, this.res) + }) - this.CompileManager.syncFromCode = sinon.stub().callsArgWith(5, null, (this.pdfPositions = ["mock-positions"])); - return this.CompileController.syncFromCode(this.req, this.res, this.next); - }); + return it('should return the JSON response with the timeout status', function() { + this.res.status.calledWith(200).should.equal(true) + return this.res.send + .calledWith({ + compile: { + status: 'timedout', + error: this.message, + outputFiles: [] + } + }) + .should.equal(true) + }) + }) - it("should find the corresponding location in the PDF", function() { - return this.CompileManager.syncFromCode - .calledWith(this.project_id, undefined, this.file, this.line, this.column) - .should.equal(true); - }); + return describe('when the request returns no output files', function() { + beforeEach(function() { + this.CompileManager.doCompileWithLock = sinon + .stub() + .callsArgWith(1, null, []) + return this.CompileController.compile(this.req, this.res) + }) - return it("should return the positions", function() { - return this.res.json - .calledWith({ - pdf: this.pdfPositions - }) - .should.equal(true); - }); - }); + return it('should return the JSON response with the failure status', function() { + this.res.status.calledWith(200).should.equal(true) + return this.res.send + .calledWith({ + compile: { + error: null, + status: 'failure', + outputFiles: [] + } + }) + .should.equal(true) + }) + }) + }) - describe("syncFromPdf", function() { - beforeEach(function() { - this.page = 5; - this.h = 100.23; - this.v = 45.67; - this.project_id = "mock-project-id"; - this.req.params = - {project_id: this.project_id}; - this.req.query = { - page: this.page.toString(), - h: this.h.toString(), - v: this.v.toString() - }; - this.res.json = sinon.stub(); + describe('syncFromCode', function() { + beforeEach(function() { + this.file = 'main.tex' + this.line = 42 + this.column = 5 + this.project_id = 'mock-project-id' + this.req.params = { project_id: this.project_id } + this.req.query = { + file: this.file, + line: this.line.toString(), + column: this.column.toString() + } + this.res.json = sinon.stub() - this.CompileManager.syncFromPdf = sinon.stub().callsArgWith(5, null, (this.codePositions = ["mock-positions"])); - return this.CompileController.syncFromPdf(this.req, this.res, this.next); - }); + this.CompileManager.syncFromCode = sinon + .stub() + .callsArgWith(5, null, (this.pdfPositions = ['mock-positions'])) + return this.CompileController.syncFromCode(this.req, this.res, this.next) + }) - it("should find the corresponding location in the code", function() { - return this.CompileManager.syncFromPdf - .calledWith(this.project_id, undefined, this.page, this.h, this.v) - .should.equal(true); - }); + it('should find the corresponding location in the PDF', function() { + return this.CompileManager.syncFromCode + .calledWith( + this.project_id, + undefined, + this.file, + this.line, + this.column + ) + .should.equal(true) + }) - return it("should return the positions", function() { - return this.res.json - .calledWith({ - code: this.codePositions - }) - .should.equal(true); - }); - }); + return it('should return the positions', function() { + return this.res.json + .calledWith({ + pdf: this.pdfPositions + }) + .should.equal(true) + }) + }) - return describe("wordcount", function() { - beforeEach(function() { - this.file = "main.tex"; - this.project_id = "mock-project-id"; - this.req.params = - {project_id: this.project_id}; - this.req.query = { - file: this.file, - image: (this.image = "example.com/image") - }; - this.res.json = sinon.stub(); + describe('syncFromPdf', function() { + beforeEach(function() { + this.page = 5 + this.h = 100.23 + this.v = 45.67 + this.project_id = 'mock-project-id' + this.req.params = { project_id: this.project_id } + this.req.query = { + page: this.page.toString(), + h: this.h.toString(), + v: this.v.toString() + } + this.res.json = sinon.stub() - this.CompileManager.wordcount = sinon.stub().callsArgWith(4, null, (this.texcount = ["mock-texcount"])); - return this.CompileController.wordcount(this.req, this.res, this.next); - }); + this.CompileManager.syncFromPdf = sinon + .stub() + .callsArgWith(5, null, (this.codePositions = ['mock-positions'])) + return this.CompileController.syncFromPdf(this.req, this.res, this.next) + }) - it("should return the word count of a file", function() { - return this.CompileManager.wordcount - .calledWith(this.project_id, undefined, this.file, this.image) - .should.equal(true); - }); + it('should find the corresponding location in the code', function() { + return this.CompileManager.syncFromPdf + .calledWith(this.project_id, undefined, this.page, this.h, this.v) + .should.equal(true) + }) - return it("should return the texcount info", function() { - return this.res.json - .calledWith({ - texcount: this.texcount - }) - .should.equal(true); - }); - }); -}); + return it('should return the positions', function() { + return this.res.json + .calledWith({ + code: this.codePositions + }) + .should.equal(true) + }) + }) + + return describe('wordcount', function() { + beforeEach(function() { + this.file = 'main.tex' + this.project_id = 'mock-project-id' + this.req.params = { project_id: this.project_id } + this.req.query = { + file: this.file, + image: (this.image = 'example.com/image') + } + this.res.json = sinon.stub() + + this.CompileManager.wordcount = sinon + .stub() + .callsArgWith(4, null, (this.texcount = ['mock-texcount'])) + return this.CompileController.wordcount(this.req, this.res, this.next) + }) + + it('should return the word count of a file', function() { + return this.CompileManager.wordcount + .calledWith(this.project_id, undefined, this.file, this.image) + .should.equal(true) + }) + + return it('should return the texcount info', function() { + return this.res.json + .calledWith({ + texcount: this.texcount + }) + .should.equal(true) + }) + }) +}) diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index e798aec3..ae50bcc6 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -13,423 +13,539 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/CompileManager'); -const tk = require("timekeeper"); -const { EventEmitter } = require("events"); -const Path = require("path"); - -describe("CompileManager", function() { - beforeEach(function() { - this.CompileManager = SandboxedModule.require(modulePath, { requires: { - "./LatexRunner": (this.LatexRunner = {}), - "./ResourceWriter": (this.ResourceWriter = {}), - "./OutputFileFinder": (this.OutputFileFinder = {}), - "./OutputCacheManager": (this.OutputCacheManager = {}), - "settings-sharelatex": (this.Settings = { - path: { - compilesDir: "/compiles/dir" - }, - synctexBaseDir() { return "/compile"; }, - clsi: { - docker: { - image: "SOMEIMAGE" - } - } - }), - - "logger-sharelatex": (this.logger = { log: sinon.stub() , info() {}}), - "child_process": (this.child_process = {}), - "./CommandRunner": (this.CommandRunner = {}), - "./DraftModeManager": (this.DraftModeManager = {}), - "./TikzManager": (this.TikzManager = {}), - "./LockManager": (this.LockManager = {}), - "fs": (this.fs = {}), - "fs-extra": (this.fse = { ensureDir: sinon.stub().callsArg(1) }) - } - }); - this.callback = sinon.stub(); - this.project_id = "project-id-123"; - return this.user_id = "1234"; - }); - describe("doCompileWithLock", function() { - beforeEach(function() { - this.request = { - resources: (this.resources = "mock-resources"), - project_id: this.project_id, - user_id: this.user_id - }; - this.output_files = ["foo", "bar"]; - this.Settings.compileDir = "compiles"; - this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; - this.CompileManager.doCompile = sinon.stub().callsArgWith(1, null, this.output_files); - return this.LockManager.runWithLock = (lockFile, runner, callback) => - runner((err, ...result) => callback(err, ...Array.from(result))) - ; - }); - - describe("when the project is not locked", function() { - beforeEach(function() { - return this.CompileManager.doCompileWithLock(this.request, this.callback); - }); - - it("should ensure that the compile directory exists", function() { - return this.fse.ensureDir.calledWith(this.compileDir) - .should.equal(true); - }); - - it("should call doCompile with the request", function() { - return this.CompileManager.doCompile - .calledWith(this.request) - .should.equal(true); - }); - - return it("should call the callback with the output files", function() { - return this.callback.calledWithExactly(null, this.output_files) - .should.equal(true); - }); - }); - - return describe("when the project is locked", function() { - beforeEach(function() { - this.error = new Error("locked"); - this.LockManager.runWithLock = (lockFile, runner, callback) => { - return callback(this.error); - }; - return this.CompileManager.doCompileWithLock(this.request, this.callback); - }); - - it("should ensure that the compile directory exists", function() { - return this.fse.ensureDir.calledWith(this.compileDir) - .should.equal(true); - }); - - it("should not call doCompile with the request", function() { - return this.CompileManager.doCompile - .called.should.equal(false); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWithExactly(this.error) - .should.equal(true); - }); - }); - }); - - describe("doCompile", function() { - beforeEach(function() { - this.output_files = [{ - path: "output.log", - type: "log" - }, { - path: "output.pdf", - type: "pdf" - }]; - this.build_files = [{ - path: "output.log", - type: "log", - build: 1234 - }, { - path: "output.pdf", - type: "pdf", - build: 1234 - }]; - this.request = { - resources: (this.resources = "mock-resources"), - rootResourcePath: (this.rootResourcePath = "main.tex"), - project_id: this.project_id, - user_id: this.user_id, - compiler: (this.compiler = "pdflatex"), - timeout: (this.timeout = 42000), - imageName: (this.image = "example.com/image"), - flags: (this.flags = ["-file-line-error"]) - }; - this.env = {}; - this.Settings.compileDir = "compiles"; - this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; - this.ResourceWriter.syncResourcesToDisk = sinon.stub().callsArgWith(2, null, this.resources); - this.LatexRunner.runLatex = sinon.stub().callsArg(2); - this.OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, this.output_files); - this.OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, this.build_files); - this.DraftModeManager.injectDraftMode = sinon.stub().callsArg(1); - return this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false); - }); - - describe("normally", function() { - beforeEach(function() { - return this.CompileManager.doCompile(this.request, this.callback); - }); - - it("should write the resources to disk", function() { - return this.ResourceWriter.syncResourcesToDisk - .calledWith(this.request, this.compileDir) - .should.equal(true); - }); - - it("should run LaTeX", function() { - return this.LatexRunner.runLatex - .calledWith(`${this.project_id}-${this.user_id}`, { - directory: this.compileDir, - mainFile: this.rootResourcePath, - compiler: this.compiler, - timeout: this.timeout, - image: this.image, - flags: this.flags, - environment: this.env - }) - .should.equal(true); - }); - - it("should find the output files", function() { - return this.OutputFileFinder.findOutputFiles - .calledWith(this.resources, this.compileDir) - .should.equal(true); - }); - - it("should return the output files", function() { - return this.callback.calledWith(null, this.build_files).should.equal(true); - }); - - return it("should not inject draft mode by default", function() { - return this.DraftModeManager.injectDraftMode.called.should.equal(false); - }); - }); - - describe("with draft mode", function() { - beforeEach(function() { - this.request.draft = true; - return this.CompileManager.doCompile(this.request, this.callback); - }); - - return it("should inject the draft mode header", function() { - return this.DraftModeManager.injectDraftMode - .calledWith(this.compileDir + "/" + this.rootResourcePath) - .should.equal(true); - }); - }); - - describe("with a check option", function() { - beforeEach(function() { - this.request.check = "error"; - return this.CompileManager.doCompile(this.request, this.callback); - }); - - return it("should run chktex", function() { - return this.LatexRunner.runLatex - .calledWith(`${this.project_id}-${this.user_id}`, { - directory: this.compileDir, - mainFile: this.rootResourcePath, - compiler: this.compiler, - timeout: this.timeout, - image: this.image, - flags: this.flags, - environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1, 'CHKTEX_ULIMIT_OPTIONS': '-t 5 -v 64000'} - }) - .should.equal(true); - }); - }); - - return describe("with a knitr file and check options", function() { - beforeEach(function() { - this.request.rootResourcePath = "main.Rtex"; - this.request.check = "error"; - return this.CompileManager.doCompile(this.request, this.callback); - }); - - return it("should not run chktex", function() { - return this.LatexRunner.runLatex - .calledWith(`${this.project_id}-${this.user_id}`, { - directory: this.compileDir, - mainFile: "main.Rtex", - compiler: this.compiler, - timeout: this.timeout, - image: this.image, - flags: this.flags, - environment: this.env - }) - .should.equal(true); - }); - }); - }); - - describe("clearProject", function() { - describe("succesfully", function() { - beforeEach(function() { - this.Settings.compileDir = "compiles"; - this.fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory(){ return true; }}); - this.proc = new EventEmitter(); - this.proc.stdout = new EventEmitter(); - this.proc.stderr = new EventEmitter(); - this.child_process.spawn = sinon.stub().returns(this.proc); - this.CompileManager.clearProject(this.project_id, this.user_id, this.callback); - return this.proc.emit("close", 0); - }); - - it("should remove the project directory", function() { - return this.child_process.spawn - .calledWith("rm", ["-r", `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`]) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("with a non-success status code", function() { - beforeEach(function() { - this.Settings.compileDir = "compiles"; - this.fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory(){ return true; }}); - this.proc = new EventEmitter(); - this.proc.stdout = new EventEmitter(); - this.proc.stderr = new EventEmitter(); - this.child_process.spawn = sinon.stub().returns(this.proc); - this.CompileManager.clearProject(this.project_id, this.user_id, this.callback); - this.proc.stderr.emit("data", (this.error = "oops")); - return this.proc.emit("close", 1); - }); - - it("should remove the project directory", function() { - return this.child_process.spawn - .calledWith("rm", ["-r", `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`]) - .should.equal(true); - }); - - return it("should call the callback with an error from the stderr", function() { - this.callback - .calledWith(new Error()) - .should.equal(true); - - return this.callback.args[0][0].message.should.equal(`rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} failed: ${this.error}`); - }); - }); - }); - - describe("syncing", function() { - beforeEach(function() { - this.page = 1; - this.h = 42.23; - this.v = 87.56; - this.width = 100.01; - this.height = 234.56; - this.line = 5; - this.column = 3; - this.file_name = "main.tex"; - this.child_process.execFile = sinon.stub(); - return this.Settings.path.synctexBaseDir = project_id => `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; - }); - - describe("syncFromCode", function() { - beforeEach(function() { - this.fs.stat = sinon.stub().callsArgWith(1, null,{isFile(){ return true; }}); - this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n`; - this.CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:this.stdout}); - return this.CompileManager.syncFromCode(this.project_id, this.user_id, this.file_name, this.line, this.column, this.callback); - }); - - it("should execute the synctex binary", function() { - const bin_path = Path.resolve(__dirname + "/../../../bin/synctex"); - const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`; - const file_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}`; - return this.CommandRunner.run - .calledWith( - `${this.project_id}-${this.user_id}`, - ['/opt/synctex', 'code', synctex_path, file_path, this.line, this.column], - `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, - this.Settings.clsi.docker.image, - 60000, - {} - ).should.equal(true); - }); - - return it("should call the callback with the parsed output", function() { - return this.callback - .calledWith(null, [{ - page: this.page, - h: this.h, - v: this.v, - height: this.height, - width: this.width - }]) - .should.equal(true); - }); - }); - - return describe("syncFromPdf", function() { - beforeEach(function() { - this.fs.stat = sinon.stub().callsArgWith(1, null,{isFile(){ return true; }}); - this.stdout = `NODE\t${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}\t${this.line}\t${this.column}\n`; - this.CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:this.stdout}); - return this.CompileManager.syncFromPdf(this.project_id, this.user_id, this.page, this.h, this.v, this.callback); - }); - - it("should execute the synctex binary", function() { - const bin_path = Path.resolve(__dirname + "/../../../bin/synctex"); - const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`; - return this.CommandRunner.run - .calledWith( - `${this.project_id}-${this.user_id}`, - ['/opt/synctex', "pdf", synctex_path, this.page, this.h, this.v], - `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, - this.Settings.clsi.docker.image, - 60000, - {}).should.equal(true); - }); - - return it("should call the callback with the parsed output", function() { - return this.callback - .calledWith(null, [{ - file: this.file_name, - line: this.line, - column: this.column - }]) - .should.equal(true); - }); - }); - }); - - return describe("wordcount", function() { - beforeEach(function() { - this.CommandRunner.run = sinon.stub().callsArg(6); - this.fs.readFile = sinon.stub().callsArgWith(2, null, (this.stdout = "Encoding: ascii\nWords in text: 2")); - this.callback = sinon.stub(); - - this.project_id; - this.timeout = 60 * 1000; - this.file_name = "main.tex"; - this.Settings.path.compilesDir = "/local/compile/directory"; - this.image = "example.com/image"; - - return this.CompileManager.wordcount(this.project_id, this.user_id, this.file_name, this.image, this.callback); - }); - - it("should run the texcount command", function() { - this.directory = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`; - this.file_path = `$COMPILE_DIR/${this.file_name}`; - this.command =[ "texcount", "-nocol", "-inc", this.file_path, `-out=${this.file_path}.wc`]; - - return this.CommandRunner.run - .calledWith(`${this.project_id}-${this.user_id}`, this.command, this.directory, this.image, this.timeout, {}) - .should.equal(true); - }); - - return it("should call the callback with the parsed output", function() { - return this.callback - .calledWith(null, { - encode: "ascii", - textWords: 2, - headWords: 0, - outside: 0, - headers: 0, - elements: 0, - mathInline: 0, - mathDisplay: 0, - errors: 0, - messages: "" - }) - .should.equal(true); - }); - }); -}); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/CompileManager' +) +const tk = require('timekeeper') +const { EventEmitter } = require('events') +const Path = require('path') + +describe('CompileManager', function() { + beforeEach(function() { + this.CompileManager = SandboxedModule.require(modulePath, { + requires: { + './LatexRunner': (this.LatexRunner = {}), + './ResourceWriter': (this.ResourceWriter = {}), + './OutputFileFinder': (this.OutputFileFinder = {}), + './OutputCacheManager': (this.OutputCacheManager = {}), + 'settings-sharelatex': (this.Settings = { + path: { + compilesDir: '/compiles/dir' + }, + synctexBaseDir() { + return '/compile' + }, + clsi: { + docker: { + image: 'SOMEIMAGE' + } + } + }), + + 'logger-sharelatex': (this.logger = { log: sinon.stub(), info() {} }), + child_process: (this.child_process = {}), + './CommandRunner': (this.CommandRunner = {}), + './DraftModeManager': (this.DraftModeManager = {}), + './TikzManager': (this.TikzManager = {}), + './LockManager': (this.LockManager = {}), + fs: (this.fs = {}), + 'fs-extra': (this.fse = { ensureDir: sinon.stub().callsArg(1) }) + } + }) + this.callback = sinon.stub() + this.project_id = 'project-id-123' + return (this.user_id = '1234') + }) + describe('doCompileWithLock', function() { + beforeEach(function() { + this.request = { + resources: (this.resources = 'mock-resources'), + project_id: this.project_id, + user_id: this.user_id + } + this.output_files = ['foo', 'bar'] + this.Settings.compileDir = 'compiles' + this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + this.CompileManager.doCompile = sinon + .stub() + .callsArgWith(1, null, this.output_files) + return (this.LockManager.runWithLock = (lockFile, runner, callback) => + runner((err, ...result) => callback(err, ...Array.from(result)))) + }) + + describe('when the project is not locked', function() { + beforeEach(function() { + return this.CompileManager.doCompileWithLock( + this.request, + this.callback + ) + }) + + it('should ensure that the compile directory exists', function() { + return this.fse.ensureDir.calledWith(this.compileDir).should.equal(true) + }) + + it('should call doCompile with the request', function() { + return this.CompileManager.doCompile + .calledWith(this.request) + .should.equal(true) + }) + + return it('should call the callback with the output files', function() { + return this.callback + .calledWithExactly(null, this.output_files) + .should.equal(true) + }) + }) + + return describe('when the project is locked', function() { + beforeEach(function() { + this.error = new Error('locked') + this.LockManager.runWithLock = (lockFile, runner, callback) => { + return callback(this.error) + } + return this.CompileManager.doCompileWithLock( + this.request, + this.callback + ) + }) + + it('should ensure that the compile directory exists', function() { + return this.fse.ensureDir.calledWith(this.compileDir).should.equal(true) + }) + + it('should not call doCompile with the request', function() { + return this.CompileManager.doCompile.called.should.equal(false) + }) + + return it('should call the callback with the error', function() { + return this.callback.calledWithExactly(this.error).should.equal(true) + }) + }) + }) + + describe('doCompile', function() { + beforeEach(function() { + this.output_files = [ + { + path: 'output.log', + type: 'log' + }, + { + path: 'output.pdf', + type: 'pdf' + } + ] + this.build_files = [ + { + path: 'output.log', + type: 'log', + build: 1234 + }, + { + path: 'output.pdf', + type: 'pdf', + build: 1234 + } + ] + this.request = { + resources: (this.resources = 'mock-resources'), + rootResourcePath: (this.rootResourcePath = 'main.tex'), + project_id: this.project_id, + user_id: this.user_id, + compiler: (this.compiler = 'pdflatex'), + timeout: (this.timeout = 42000), + imageName: (this.image = 'example.com/image'), + flags: (this.flags = ['-file-line-error']) + } + this.env = {} + this.Settings.compileDir = 'compiles' + this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + this.ResourceWriter.syncResourcesToDisk = sinon + .stub() + .callsArgWith(2, null, this.resources) + this.LatexRunner.runLatex = sinon.stub().callsArg(2) + this.OutputFileFinder.findOutputFiles = sinon + .stub() + .callsArgWith(2, null, this.output_files) + this.OutputCacheManager.saveOutputFiles = sinon + .stub() + .callsArgWith(2, null, this.build_files) + this.DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) + return (this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false)) + }) + + describe('normally', function() { + beforeEach(function() { + return this.CompileManager.doCompile(this.request, this.callback) + }) + + it('should write the resources to disk', function() { + return this.ResourceWriter.syncResourcesToDisk + .calledWith(this.request, this.compileDir) + .should.equal(true) + }) + + it('should run LaTeX', function() { + return this.LatexRunner.runLatex + .calledWith(`${this.project_id}-${this.user_id}`, { + directory: this.compileDir, + mainFile: this.rootResourcePath, + compiler: this.compiler, + timeout: this.timeout, + image: this.image, + flags: this.flags, + environment: this.env + }) + .should.equal(true) + }) + + it('should find the output files', function() { + return this.OutputFileFinder.findOutputFiles + .calledWith(this.resources, this.compileDir) + .should.equal(true) + }) + + it('should return the output files', function() { + return this.callback + .calledWith(null, this.build_files) + .should.equal(true) + }) + + return it('should not inject draft mode by default', function() { + return this.DraftModeManager.injectDraftMode.called.should.equal(false) + }) + }) + + describe('with draft mode', function() { + beforeEach(function() { + this.request.draft = true + return this.CompileManager.doCompile(this.request, this.callback) + }) + + return it('should inject the draft mode header', function() { + return this.DraftModeManager.injectDraftMode + .calledWith(this.compileDir + '/' + this.rootResourcePath) + .should.equal(true) + }) + }) + + describe('with a check option', function() { + beforeEach(function() { + this.request.check = 'error' + return this.CompileManager.doCompile(this.request, this.callback) + }) + + return it('should run chktex', function() { + return this.LatexRunner.runLatex + .calledWith(`${this.project_id}-${this.user_id}`, { + directory: this.compileDir, + mainFile: this.rootResourcePath, + compiler: this.compiler, + timeout: this.timeout, + image: this.image, + flags: this.flags, + environment: { + CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16', + CHKTEX_EXIT_ON_ERROR: 1, + CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000' + } + }) + .should.equal(true) + }) + }) + + return describe('with a knitr file and check options', function() { + beforeEach(function() { + this.request.rootResourcePath = 'main.Rtex' + this.request.check = 'error' + return this.CompileManager.doCompile(this.request, this.callback) + }) + + return it('should not run chktex', function() { + return this.LatexRunner.runLatex + .calledWith(`${this.project_id}-${this.user_id}`, { + directory: this.compileDir, + mainFile: 'main.Rtex', + compiler: this.compiler, + timeout: this.timeout, + image: this.image, + flags: this.flags, + environment: this.env + }) + .should.equal(true) + }) + }) + }) + + describe('clearProject', function() { + describe('succesfully', function() { + beforeEach(function() { + this.Settings.compileDir = 'compiles' + this.fs.lstat = sinon.stub().callsArgWith(1, null, { + isDirectory() { + return true + } + }) + this.proc = new EventEmitter() + this.proc.stdout = new EventEmitter() + this.proc.stderr = new EventEmitter() + this.child_process.spawn = sinon.stub().returns(this.proc) + this.CompileManager.clearProject( + this.project_id, + this.user_id, + this.callback + ) + return this.proc.emit('close', 0) + }) + + it('should remove the project directory', function() { + return this.child_process.spawn + .calledWith('rm', [ + '-r', + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + ]) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('with a non-success status code', function() { + beforeEach(function() { + this.Settings.compileDir = 'compiles' + this.fs.lstat = sinon.stub().callsArgWith(1, null, { + isDirectory() { + return true + } + }) + this.proc = new EventEmitter() + this.proc.stdout = new EventEmitter() + this.proc.stderr = new EventEmitter() + this.child_process.spawn = sinon.stub().returns(this.proc) + this.CompileManager.clearProject( + this.project_id, + this.user_id, + this.callback + ) + this.proc.stderr.emit('data', (this.error = 'oops')) + return this.proc.emit('close', 1) + }) + + it('should remove the project directory', function() { + return this.child_process.spawn + .calledWith('rm', [ + '-r', + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + ]) + .should.equal(true) + }) + + return it('should call the callback with an error from the stderr', function() { + this.callback.calledWith(new Error()).should.equal(true) + + return this.callback.args[0][0].message.should.equal( + `rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} failed: ${this.error}` + ) + }) + }) + }) + + describe('syncing', function() { + beforeEach(function() { + this.page = 1 + this.h = 42.23 + this.v = 87.56 + this.width = 100.01 + this.height = 234.56 + this.line = 5 + this.column = 3 + this.file_name = 'main.tex' + this.child_process.execFile = sinon.stub() + return (this.Settings.path.synctexBaseDir = project_id => + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`) + }) + + describe('syncFromCode', function() { + beforeEach(function() { + this.fs.stat = sinon.stub().callsArgWith(1, null, { + isFile() { + return true + } + }) + this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n` + this.CommandRunner.run = sinon + .stub() + .callsArgWith(6, null, { stdout: this.stdout }) + return this.CompileManager.syncFromCode( + this.project_id, + this.user_id, + this.file_name, + this.line, + this.column, + this.callback + ) + }) + + it('should execute the synctex binary', function() { + const bin_path = Path.resolve(__dirname + '/../../../bin/synctex') + const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf` + const file_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}` + return this.CommandRunner.run + .calledWith( + `${this.project_id}-${this.user_id}`, + [ + '/opt/synctex', + 'code', + synctex_path, + file_path, + this.line, + this.column + ], + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + this.Settings.clsi.docker.image, + 60000, + {} + ) + .should.equal(true) + }) + + return it('should call the callback with the parsed output', function() { + return this.callback + .calledWith(null, [ + { + page: this.page, + h: this.h, + v: this.v, + height: this.height, + width: this.width + } + ]) + .should.equal(true) + }) + }) + + return describe('syncFromPdf', function() { + beforeEach(function() { + this.fs.stat = sinon.stub().callsArgWith(1, null, { + isFile() { + return true + } + }) + this.stdout = `NODE\t${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}\t${this.line}\t${this.column}\n` + this.CommandRunner.run = sinon + .stub() + .callsArgWith(6, null, { stdout: this.stdout }) + return this.CompileManager.syncFromPdf( + this.project_id, + this.user_id, + this.page, + this.h, + this.v, + this.callback + ) + }) + + it('should execute the synctex binary', function() { + const bin_path = Path.resolve(__dirname + '/../../../bin/synctex') + const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf` + return this.CommandRunner.run + .calledWith( + `${this.project_id}-${this.user_id}`, + ['/opt/synctex', 'pdf', synctex_path, this.page, this.h, this.v], + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + this.Settings.clsi.docker.image, + 60000, + {} + ) + .should.equal(true) + }) + + return it('should call the callback with the parsed output', function() { + return this.callback + .calledWith(null, [ + { + file: this.file_name, + line: this.line, + column: this.column + } + ]) + .should.equal(true) + }) + }) + }) + + return describe('wordcount', function() { + beforeEach(function() { + this.CommandRunner.run = sinon.stub().callsArg(6) + this.fs.readFile = sinon + .stub() + .callsArgWith( + 2, + null, + (this.stdout = 'Encoding: ascii\nWords in text: 2') + ) + this.callback = sinon.stub() + + this.project_id + this.timeout = 60 * 1000 + this.file_name = 'main.tex' + this.Settings.path.compilesDir = '/local/compile/directory' + this.image = 'example.com/image' + + return this.CompileManager.wordcount( + this.project_id, + this.user_id, + this.file_name, + this.image, + this.callback + ) + }) + + it('should run the texcount command', function() { + this.directory = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + this.file_path = `$COMPILE_DIR/${this.file_name}` + this.command = [ + 'texcount', + '-nocol', + '-inc', + this.file_path, + `-out=${this.file_path}.wc` + ] + + return this.CommandRunner.run + .calledWith( + `${this.project_id}-${this.user_id}`, + this.command, + this.directory, + this.image, + this.timeout, + {} + ) + .should.equal(true) + }) + + return it('should call the callback with the parsed output', function() { + return this.callback + .calledWith(null, { + encode: 'ascii', + textWords: 2, + headWords: 0, + outside: 0, + headers: 0, + elements: 0, + mathInline: 0, + mathDisplay: 0, + errors: 0, + messages: '' + }) + .should.equal(true) + }) + }) +}) diff --git a/test/unit/js/ContentTypeMapperTests.js b/test/unit/js/ContentTypeMapperTests.js index bbde2923..41fc37e4 100644 --- a/test/unit/js/ContentTypeMapperTests.js +++ b/test/unit/js/ContentTypeMapperTests.js @@ -10,73 +10,72 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/ContentTypeMapper'); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/ContentTypeMapper' +) describe('ContentTypeMapper', function() { + beforeEach(function() { + return (this.ContentTypeMapper = SandboxedModule.require(modulePath)) + }) - beforeEach(function() { - return this.ContentTypeMapper = SandboxedModule.require(modulePath); - }); + return describe('map', function() { + it('should map .txt to text/plain', function() { + const content_type = this.ContentTypeMapper.map('example.txt') + return content_type.should.equal('text/plain') + }) - return describe('map', function() { + it('should map .csv to text/csv', function() { + const content_type = this.ContentTypeMapper.map('example.csv') + return content_type.should.equal('text/csv') + }) - it('should map .txt to text/plain', function() { - const content_type = this.ContentTypeMapper.map('example.txt'); - return content_type.should.equal('text/plain'); - }); + it('should map .pdf to application/pdf', function() { + const content_type = this.ContentTypeMapper.map('example.pdf') + return content_type.should.equal('application/pdf') + }) - it('should map .csv to text/csv', function() { - const content_type = this.ContentTypeMapper.map('example.csv'); - return content_type.should.equal('text/csv'); - }); + it('should fall back to octet-stream', function() { + const content_type = this.ContentTypeMapper.map('example.unknown') + return content_type.should.equal('application/octet-stream') + }) - it('should map .pdf to application/pdf', function() { - const content_type = this.ContentTypeMapper.map('example.pdf'); - return content_type.should.equal('application/pdf'); - }); + describe('coercing web files to plain text', function() { + it('should map .js to plain text', function() { + const content_type = this.ContentTypeMapper.map('example.js') + return content_type.should.equal('text/plain') + }) - it('should fall back to octet-stream', function() { - const content_type = this.ContentTypeMapper.map('example.unknown'); - return content_type.should.equal('application/octet-stream'); - }); + it('should map .html to plain text', function() { + const content_type = this.ContentTypeMapper.map('example.html') + return content_type.should.equal('text/plain') + }) - describe('coercing web files to plain text', function() { + return it('should map .css to plain text', function() { + const content_type = this.ContentTypeMapper.map('example.css') + return content_type.should.equal('text/plain') + }) + }) - it('should map .js to plain text', function() { - const content_type = this.ContentTypeMapper.map('example.js'); - return content_type.should.equal('text/plain'); - }); + return describe('image files', function() { + it('should map .png to image/png', function() { + const content_type = this.ContentTypeMapper.map('example.png') + return content_type.should.equal('image/png') + }) - it('should map .html to plain text', function() { - const content_type = this.ContentTypeMapper.map('example.html'); - return content_type.should.equal('text/plain'); - }); + it('should map .jpeg to image/jpeg', function() { + const content_type = this.ContentTypeMapper.map('example.jpeg') + return content_type.should.equal('image/jpeg') + }) - return it('should map .css to plain text', function() { - const content_type = this.ContentTypeMapper.map('example.css'); - return content_type.should.equal('text/plain'); - }); - }); - - return describe('image files', function() { - - it('should map .png to image/png', function() { - const content_type = this.ContentTypeMapper.map('example.png'); - return content_type.should.equal('image/png'); - }); - - it('should map .jpeg to image/jpeg', function() { - const content_type = this.ContentTypeMapper.map('example.jpeg'); - return content_type.should.equal('image/jpeg'); - }); - - return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function() { - const content_type = this.ContentTypeMapper.map('example.svg'); - return content_type.should.equal('text/plain'); - }); - }); - }); -}); + return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function() { + const content_type = this.ContentTypeMapper.map('example.svg') + return content_type.should.equal('text/plain') + }) + }) + }) +}) diff --git a/test/unit/js/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js index 155a2464..9dcf9dcc 100644 --- a/test/unit/js/DockerLockManagerTests.js +++ b/test/unit/js/DockerLockManagerTests.js @@ -9,185 +9,244 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -require("coffee-script"); -const modulePath = require('path').join(__dirname, '../../../app/coffee/DockerLockManager'); - -describe("LockManager", function() { - beforeEach(function() { - return this.LockManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.Settings = - {clsi: {docker: {}}}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }) - } - });}); - - return describe("runWithLock", function() { - describe("with a single lock", function() { - beforeEach(function(done) { - this.callback = sinon.stub(); - return this.LockManager.runWithLock("lock-one", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world") - , 100) - - , (err, ...args) => { - this.callback(err,...Array.from(args)); - return done(); - }); - }); - - return it("should call the callback", function() { - return this.callback.calledWith(null,"hello","world").should.equal(true); - }); - }); - - describe("with two locks", function() { - beforeEach(function(done) { - this.callback1 = sinon.stub(); - this.callback2 = sinon.stub(); - this.LockManager.runWithLock("lock-one", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","one") - , 100) - - , (err, ...args) => { - return this.callback1(err,...Array.from(args)); - }); - return this.LockManager.runWithLock("lock-two", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","two") - , 200) - - , (err, ...args) => { - this.callback2(err,...Array.from(args)); - return done(); - }); - }); - - it("should call the first callback", function() { - return this.callback1.calledWith(null,"hello","world","one").should.equal(true); - }); - - return it("should call the second callback", function() { - return this.callback2.calledWith(null,"hello","world","two").should.equal(true); - }); - }); - - return describe("with lock contention", function() { - describe("where the first lock is released quickly", function() { - beforeEach(function(done) { - this.LockManager.MAX_LOCK_WAIT_TIME = 1000; - this.LockManager.LOCK_TEST_INTERVAL = 100; - this.callback1 = sinon.stub(); - this.callback2 = sinon.stub(); - this.LockManager.runWithLock("lock", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","one") - , 100) - - , (err, ...args) => { - return this.callback1(err,...Array.from(args)); - }); - return this.LockManager.runWithLock("lock", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","two") - , 200) - - , (err, ...args) => { - this.callback2(err,...Array.from(args)); - return done(); - }); - }); - - it("should call the first callback", function() { - return this.callback1.calledWith(null,"hello","world","one").should.equal(true); - }); - - return it("should call the second callback", function() { - return this.callback2.calledWith(null,"hello","world","two").should.equal(true); - }); - }); - - describe("where the first lock is held longer than the waiting time", function() { - beforeEach(function(done) { - let doneTwo; - this.LockManager.MAX_LOCK_HOLD_TIME = 10000; - this.LockManager.MAX_LOCK_WAIT_TIME = 1000; - this.LockManager.LOCK_TEST_INTERVAL = 100; - this.callback1 = sinon.stub(); - this.callback2 = sinon.stub(); - let doneOne = (doneTwo = false); - const finish = function(key) { - if (key === 1) { doneOne = true; } - if (key === 2) { doneTwo = true; } - if (doneOne && doneTwo) { return done(); } - }; - this.LockManager.runWithLock("lock", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","one") - , 1100) - - , (err, ...args) => { - this.callback1(err,...Array.from(args)); - return finish(1); - }); - return this.LockManager.runWithLock("lock", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","two") - , 100) - - , (err, ...args) => { - this.callback2(err,...Array.from(args)); - return finish(2); - }); - }); - - it("should call the first callback", function() { - return this.callback1.calledWith(null,"hello","world","one").should.equal(true); - }); - - return it("should call the second callback with an error", function() { - const error = sinon.match.instanceOf(Error); - return this.callback2.calledWith(error).should.equal(true); - }); - }); - - return describe("where the first lock is held longer than the max holding time", function() { - beforeEach(function(done) { - let doneTwo; - this.LockManager.MAX_LOCK_HOLD_TIME = 1000; - this.LockManager.MAX_LOCK_WAIT_TIME = 2000; - this.LockManager.LOCK_TEST_INTERVAL = 100; - this.callback1 = sinon.stub(); - this.callback2 = sinon.stub(); - let doneOne = (doneTwo = false); - const finish = function(key) { - if (key === 1) { doneOne = true; } - if (key === 2) { doneTwo = true; } - if (doneOne && doneTwo) { return done(); } - }; - this.LockManager.runWithLock("lock", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","one") - , 1500) - - , (err, ...args) => { - this.callback1(err,...Array.from(args)); - return finish(1); - }); - return this.LockManager.runWithLock("lock", releaseLock => - setTimeout(() => releaseLock(null, "hello", "world","two") - , 100) - - , (err, ...args) => { - this.callback2(err,...Array.from(args)); - return finish(2); - }); - }); - - it("should call the first callback", function() { - return this.callback1.calledWith(null,"hello","world","one").should.equal(true); - }); - - return it("should call the second callback", function() { - return this.callback2.calledWith(null,"hello","world","two").should.equal(true); - }); - }); - }); - }); -}); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +require('coffee-script') +const modulePath = require('path').join( + __dirname, + '../../../app/coffee/DockerLockManager' +) + +describe('LockManager', function() { + beforeEach(function() { + return (this.LockManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.Settings = { clsi: { docker: {} } }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }) + } + })) + }) + + return describe('runWithLock', function() { + describe('with a single lock', function() { + beforeEach(function(done) { + this.callback = sinon.stub() + return this.LockManager.runWithLock( + 'lock-one', + releaseLock => + setTimeout(() => releaseLock(null, 'hello', 'world'), 100), + + (err, ...args) => { + this.callback(err, ...Array.from(args)) + return done() + } + ) + }) + + return it('should call the callback', function() { + return this.callback + .calledWith(null, 'hello', 'world') + .should.equal(true) + }) + }) + + describe('with two locks', function() { + beforeEach(function(done) { + this.callback1 = sinon.stub() + this.callback2 = sinon.stub() + this.LockManager.runWithLock( + 'lock-one', + releaseLock => + setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100), + + (err, ...args) => { + return this.callback1(err, ...Array.from(args)) + } + ) + return this.LockManager.runWithLock( + 'lock-two', + releaseLock => + setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200), + + (err, ...args) => { + this.callback2(err, ...Array.from(args)) + return done() + } + ) + }) + + it('should call the first callback', function() { + return this.callback1 + .calledWith(null, 'hello', 'world', 'one') + .should.equal(true) + }) + + return it('should call the second callback', function() { + return this.callback2 + .calledWith(null, 'hello', 'world', 'two') + .should.equal(true) + }) + }) + + return describe('with lock contention', function() { + describe('where the first lock is released quickly', function() { + beforeEach(function(done) { + this.LockManager.MAX_LOCK_WAIT_TIME = 1000 + this.LockManager.LOCK_TEST_INTERVAL = 100 + this.callback1 = sinon.stub() + this.callback2 = sinon.stub() + this.LockManager.runWithLock( + 'lock', + releaseLock => + setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100), + + (err, ...args) => { + return this.callback1(err, ...Array.from(args)) + } + ) + return this.LockManager.runWithLock( + 'lock', + releaseLock => + setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200), + + (err, ...args) => { + this.callback2(err, ...Array.from(args)) + return done() + } + ) + }) + + it('should call the first callback', function() { + return this.callback1 + .calledWith(null, 'hello', 'world', 'one') + .should.equal(true) + }) + + return it('should call the second callback', function() { + return this.callback2 + .calledWith(null, 'hello', 'world', 'two') + .should.equal(true) + }) + }) + + describe('where the first lock is held longer than the waiting time', function() { + beforeEach(function(done) { + let doneTwo + this.LockManager.MAX_LOCK_HOLD_TIME = 10000 + this.LockManager.MAX_LOCK_WAIT_TIME = 1000 + this.LockManager.LOCK_TEST_INTERVAL = 100 + this.callback1 = sinon.stub() + this.callback2 = sinon.stub() + let doneOne = (doneTwo = false) + const finish = function(key) { + if (key === 1) { + doneOne = true + } + if (key === 2) { + doneTwo = true + } + if (doneOne && doneTwo) { + return done() + } + } + this.LockManager.runWithLock( + 'lock', + releaseLock => + setTimeout( + () => releaseLock(null, 'hello', 'world', 'one'), + 1100 + ), + + (err, ...args) => { + this.callback1(err, ...Array.from(args)) + return finish(1) + } + ) + return this.LockManager.runWithLock( + 'lock', + releaseLock => + setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100), + + (err, ...args) => { + this.callback2(err, ...Array.from(args)) + return finish(2) + } + ) + }) + + it('should call the first callback', function() { + return this.callback1 + .calledWith(null, 'hello', 'world', 'one') + .should.equal(true) + }) + + return it('should call the second callback with an error', function() { + const error = sinon.match.instanceOf(Error) + return this.callback2.calledWith(error).should.equal(true) + }) + }) + + return describe('where the first lock is held longer than the max holding time', function() { + beforeEach(function(done) { + let doneTwo + this.LockManager.MAX_LOCK_HOLD_TIME = 1000 + this.LockManager.MAX_LOCK_WAIT_TIME = 2000 + this.LockManager.LOCK_TEST_INTERVAL = 100 + this.callback1 = sinon.stub() + this.callback2 = sinon.stub() + let doneOne = (doneTwo = false) + const finish = function(key) { + if (key === 1) { + doneOne = true + } + if (key === 2) { + doneTwo = true + } + if (doneOne && doneTwo) { + return done() + } + } + this.LockManager.runWithLock( + 'lock', + releaseLock => + setTimeout( + () => releaseLock(null, 'hello', 'world', 'one'), + 1500 + ), + + (err, ...args) => { + this.callback1(err, ...Array.from(args)) + return finish(1) + } + ) + return this.LockManager.runWithLock( + 'lock', + releaseLock => + setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100), + + (err, ...args) => { + this.callback2(err, ...Array.from(args)) + return finish(2) + } + ) + }) + + it('should call the first callback', function() { + return this.callback1 + .calledWith(null, 'hello', 'world', 'one') + .should.equal(true) + }) + + return it('should call the second callback', function() { + return this.callback2 + .calledWith(null, 'hello', 'world', 'two') + .should.equal(true) + }) + }) + }) + }) +}) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 152b8b96..e43a044c 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -13,651 +13,826 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const { expect } = require('chai'); -require("coffee-script"); -const modulePath = require('path').join(__dirname, '../../../app/coffee/DockerRunner'); -const Path = require("path"); - -describe("DockerRunner", function() { - beforeEach(function() { - let container, Docker, Timer; - this.container = (container = {}); - this.DockerRunner = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.Settings = { - clsi: { docker: {} - }, - path: {} - }), - "logger-sharelatex": (this.logger = { - log: sinon.stub(), - error: sinon.stub(), - info: sinon.stub(), - warn: sinon.stub() - }), - "dockerode": (Docker = (function() { - Docker = class Docker { - static initClass() { - this.prototype.getContainer = sinon.stub().returns(container); - this.prototype.createContainer = sinon.stub().yields(null, container); - this.prototype.listContainers = sinon.stub(); - } - }; - Docker.initClass(); - return Docker; - })()), - "fs": (this.fs = { stat: sinon.stub().yields(null,{isDirectory(){ return true; }}) }), - "./Metrics": { - Timer: (Timer = class Timer { - done() {} - }) - }, - "./LockManager": { - runWithLock(key, runner, callback) { return runner(callback); } - } - } - } - ); - this.Docker = Docker; - this.getContainer = Docker.prototype.getContainer; - this.createContainer = Docker.prototype.createContainer; - this.listContainers = Docker.prototype.listContainers; - - this.directory = "/local/compile/directory"; - this.mainFile = "main-file.tex"; - this.compiler = "pdflatex"; - this.image = "example.com/sharelatex/image:2016.2"; - this.env = {}; - this.callback = sinon.stub(); - this.project_id = "project-id-123"; - this.volumes = - {"/local/compile/directory": "/compile"}; - this.Settings.clsi.docker.image = (this.defaultImage = "default-image"); - return this.Settings.clsi.docker.env = {PATH: "mock-path"}; - }); - - describe("run", function() { - beforeEach(function(done){ - this.DockerRunner._getContainerOptions = sinon.stub().returns(this.options = {mockoptions: "foo"}); - this.DockerRunner._fingerprintContainer = sinon.stub().returns(this.fingerprint = "fingerprint"); - - this.name = `project-${this.project_id}-${this.fingerprint}`; - - this.command = ["mock", "command", "--outdir=$COMPILE_DIR"]; - this.command_with_dir = ["mock", "command", "--outdir=/compile"]; - this.timeout = 42000; - return done(); - }); - - describe("successfully", function() { - beforeEach(function(done){ - this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); - return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, (err, output)=> { - this.callback(err, output); - return done(); - }); - }); - - it("should generate the options for the container", function() { - return this.DockerRunner._getContainerOptions - .calledWith(this.command_with_dir, this.image, this.volumes, this.timeout) - .should.equal(true); - }); - - it("should generate the fingerprint from the returned options", function() { - return this.DockerRunner._fingerprintContainer - .calledWith(this.options) - .should.equal(true); - }); - - it("should do the run", function() { - return this.DockerRunner._runAndWaitForContainer - .calledWith(this.options, this.volumes, this.timeout) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.calledWith(null, this.output).should.equal(true); - }); - }); - - describe('when path.sandboxedCompilesHostDir is set', function() { - - beforeEach(function() { - this.Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles'; - this.directory = '/var/lib/sharelatex/data/compiles/xyz'; - this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); - return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback); - }); - - it('should re-write the bind directory', function() { - const volumes = this.DockerRunner._runAndWaitForContainer.lastCall.args[1]; - return expect(volumes).to.deep.equal({ - '/some/host/dir/compiles/xyz': '/compile' - }); - }); - - return it("should call the callback", function() { - return this.callback.calledWith(null, this.output).should.equal(true); - }); - }); - - describe("when the run throws an error", function() { - beforeEach(function() { - let firstTime = true; - this.output = "mock-output"; - this.DockerRunner._runAndWaitForContainer = (options, volumes, timeout, callback) => { - if (callback == null) { callback = function(error, output){}; } - if (firstTime) { - firstTime = false; - return callback(new Error("HTTP code is 500 which indicates error: server error")); - } else { - return callback(null, this.output); - } - }; - sinon.spy(this.DockerRunner, "_runAndWaitForContainer"); - this.DockerRunner.destroyContainer = sinon.stub().callsArg(3); - return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback); - }); - - it("should do the run twice", function() { - return this.DockerRunner._runAndWaitForContainer - .calledTwice.should.equal(true); - }); - - it("should destroy the container in between", function() { - return this.DockerRunner.destroyContainer - .calledWith(this.name, null) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.calledWith(null, this.output).should.equal(true); - }); - }); - - describe("with no image", function() { - beforeEach(function() { - this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); - return this.DockerRunner.run(this.project_id, this.command, this.directory, null, this.timeout, this.env, this.callback); - }); - - return it("should use the default image", function() { - return this.DockerRunner._getContainerOptions - .calledWith(this.command_with_dir, this.defaultImage, this.volumes, this.timeout) - .should.equal(true); - }); - }); - - return describe("with image override", function() { - beforeEach(function() { - this.Settings.texliveImageNameOveride = "overrideimage.com/something"; - this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output")); - return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback); - }); - - return it("should use the override and keep the tag", function() { - const image = this.DockerRunner._getContainerOptions.args[0][1]; - return image.should.equal("overrideimage.com/something/image:2016.2"); - }); - }); - }); - - describe("_runAndWaitForContainer", function() { - beforeEach(function() { - this.options = {mockoptions: "foo", name: (this.name = "mock-name")}; - this.DockerRunner.startContainer = (options, volumes, attachStreamHandler, callback) => { - attachStreamHandler(null, (this.output = "mock-output")); - return callback(null, (this.containerId = "container-id")); - }; - sinon.spy(this.DockerRunner, "startContainer"); - this.DockerRunner.waitForContainer = sinon.stub().callsArgWith(2, null, (this.exitCode = 42)); - return this.DockerRunner._runAndWaitForContainer(this.options, this.volumes, this.timeout, this.callback); - }); - - it("should create/start the container", function() { - return this.DockerRunner.startContainer - .calledWith(this.options, this.volumes) - .should.equal(true); - }); - - it("should wait for the container to finish", function() { - return this.DockerRunner.waitForContainer - .calledWith(this.name, this.timeout) - .should.equal(true); - }); - - return it("should call the callback with the output", function() { - return this.callback.calledWith(null, this.output).should.equal(true); - }); - }); - - describe("startContainer", function() { - beforeEach(function() { - this.attachStreamHandler = sinon.stub(); - this.attachStreamHandler.cock = true; - this.options = {mockoptions: "foo", name: "mock-name"}; - this.container.inspect = sinon.stub().callsArgWith(0); - this.DockerRunner.attachToContainer = (containerId, attachStreamHandler, cb)=> { - attachStreamHandler(); - return cb(); - }; - return sinon.spy(this.DockerRunner, "attachToContainer"); - }); - - - - describe("when the container exists", function() { - beforeEach(function() { - this.container.inspect = sinon.stub().callsArgWith(0); - this.container.start = sinon.stub().yields(); - - return this.DockerRunner.startContainer(this.options, this.volumes, this.callback, () => {}); - }); - - it("should start the container with the given name", function() { - this.getContainer - .calledWith(this.options.name) - .should.equal(true); - return this.container.start - .called - .should.equal(true); - }); - - it("should not try to create the container", function() { - return this.createContainer.called.should.equal(false); - }); - - it("should attach to the container", function() { - return this.DockerRunner.attachToContainer.called.should.equal(true); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should attach before the container starts", function() { - return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start); - }); - }); - - describe("when the container does not exist", function() { - beforeEach(function(){ - const exists = false; - this.container.start = sinon.stub().yields(); - this.container.inspect = sinon.stub().callsArgWith(0, {statusCode:404}); - return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); - }); - - it("should create the container", function() { - return this.createContainer - .calledWith(this.options) - .should.equal(true); - }); - - it("should call the callback and stream handler", function() { - this.attachStreamHandler.called.should.equal(true); - return this.callback.called.should.equal(true); - }); - - it("should attach to the container", function() { - return this.DockerRunner.attachToContainer.called.should.equal(true); - }); - - return it("should attach before the container starts", function() { - return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start); - }); - }); - - - describe("when the container is already running", function() { - beforeEach(function() { - const error = new Error(`HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.`); - error.statusCode = 304; - this.container.start = sinon.stub().yields(error); - this.container.inspect = sinon.stub().callsArgWith(0); - return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); - }); - - it("should not try to create the container", function() { - return this.createContainer.called.should.equal(false); - }); - - return it("should call the callback and stream handler without an error", function() { - this.attachStreamHandler.called.should.equal(true); - return this.callback.called.should.equal(true); - }); - }); - - describe("when a volume does not exist", function() { - beforeEach(function(){ - this.fs.stat = sinon.stub().yields(new Error("no such path")); - return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); - }); - - it("should not try to create the container", function() { - return this.createContainer.called.should.equal(false); - }); - - return it("should call the callback with an error", function() { - return this.callback.calledWith(new Error()).should.equal(true); - }); - }); - - describe("when a volume exists but is not a directory", function() { - beforeEach(function() { - this.fs.stat = sinon.stub().yields(null, {isDirectory() { return false; }}); - return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback); - }); - - it("should not try to create the container", function() { - return this.createContainer.called.should.equal(false); - }); - - return it("should call the callback with an error", function() { - return this.callback.calledWith(new Error()).should.equal(true); - }); - }); - - describe("when a volume does not exist, but sibling-containers are used", function() { - beforeEach(function() { - this.fs.stat = sinon.stub().yields(new Error("no such path")); - this.Settings.path.sandboxedCompilesHostDir = '/some/path'; - this.container.start = sinon.stub().yields(); - return this.DockerRunner.startContainer(this.options, this.volumes, this.callback); - }); - - afterEach(function() { - return delete this.Settings.path.sandboxedCompilesHostDir; - }); - - it("should start the container with the given name", function() { - this.getContainer - .calledWith(this.options.name) - .should.equal(true); - return this.container.start - .called - .should.equal(true); - }); - - it("should not try to create the container", function() { - return this.createContainer.called.should.equal(false); - }); - - return it("should call the callback", function() { - this.callback.called.should.equal(true); - return this.callback.calledWith(new Error()).should.equal(false); - }); - }); - - return describe("when the container tries to be created, but already has been (race condition)", function() {}); - }); - - describe("waitForContainer", function() { - beforeEach(function() { - this.containerId = "container-id"; - this.timeout = 5000; - this.container.wait = sinon.stub().yields(null, {StatusCode: (this.statusCode = 42)}); - return this.container.kill = sinon.stub().yields(); - }); - - describe("when the container returns in time", function() { - beforeEach(function() { - return this.DockerRunner.waitForContainer(this.containerId, this.timeout, this.callback); - }); - - it("should wait for the container", function() { - this.getContainer - .calledWith(this.containerId) - .should.equal(true); - return this.container.wait - .called - .should.equal(true); - }); - - return it("should call the callback with the exit", function() { - return this.callback - .calledWith(null, this.statusCode) - .should.equal(true); - }); - }); - - return describe("when the container does not return before the timeout", function() { - beforeEach(function(done) { - this.container.wait = function(callback) { - if (callback == null) { callback = function(error, exitCode) {}; } - return setTimeout(() => callback(null, {StatusCode: 42}) - , 100); - }; - this.timeout = 5; - return this.DockerRunner.waitForContainer(this.containerId, this.timeout, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); - - it("should call kill on the container", function() { - this.getContainer - .calledWith(this.containerId) - .should.equal(true); - return this.container.kill - .called - .should.equal(true); - }); - - return it("should call the callback with an error", function() { - const error = new Error("container timed out"); - error.timedout = true; - return this.callback - .calledWith(error) - .should.equal(true); - }); - }); - }); - - describe("destroyOldContainers", function() { - beforeEach(function(done) { - const oneHourInSeconds = 60 * 60; - const oneHourInMilliseconds = oneHourInSeconds * 1000; - const nowInSeconds = Date.now()/1000; - this.containers = [{ - Name: "/project-old-container-name", - Id: "old-container-id", - Created: nowInSeconds - oneHourInSeconds - 100 - }, { - Name: "/project-new-container-name", - Id: "new-container-id", - Created: (nowInSeconds - oneHourInSeconds) + 100 - }, { - Name: "/totally-not-a-project-container", - Id: "some-random-id", - Created: nowInSeconds - (2 * oneHourInSeconds ) - }]; - this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds; - this.listContainers.callsArgWith(1, null, this.containers); - this.DockerRunner.destroyContainer = sinon.stub().callsArg(3); - return this.DockerRunner.destroyOldContainers(error => { - this.callback(error); - return done(); - }); - }); - - it("should list all containers", function() { - return this.listContainers - .calledWith({all: true}) - .should.equal(true); - }); - - it("should destroy old containers", function() { - this.DockerRunner.destroyContainer - .callCount - .should.equal(1); - return this.DockerRunner.destroyContainer - .calledWith("/project-old-container-name", "old-container-id") - .should.equal(true); - }); - - it("should not destroy new containers", function() { - return this.DockerRunner.destroyContainer - .calledWith("/project-new-container-name", "new-container-id") - .should.equal(false); - }); - - it("should not destroy non-project containers", function() { - return this.DockerRunner.destroyContainer - .calledWith("/totally-not-a-project-container", "some-random-id") - .should.equal(false); - }); - - return it("should callback the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - - describe('_destroyContainer', function() { - beforeEach(function() { - this.containerId = 'some_id'; - this.fakeContainer = - {remove: sinon.stub().callsArgWith(1, null)}; - return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); - }); - - it('should get the container', function(done) { - return this.DockerRunner._destroyContainer(this.containerId, false, err => { - this.Docker.prototype.getContainer.callCount.should.equal(1); - this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true); - return done(); - }); - }); - - it('should try to force-destroy the container when shouldForce=true', function(done) { - return this.DockerRunner._destroyContainer(this.containerId, true, err => { - this.fakeContainer.remove.callCount.should.equal(1); - this.fakeContainer.remove.calledWith({force: true}).should.equal(true); - return done(); - }); - }); - - it('should not try to force-destroy the container when shouldForce=false', function(done) { - return this.DockerRunner._destroyContainer(this.containerId, false, err => { - this.fakeContainer.remove.callCount.should.equal(1); - this.fakeContainer.remove.calledWith({force: false}).should.equal(true); - return done(); - }); - }); - - it('should not produce an error', function(done) { - return this.DockerRunner._destroyContainer(this.containerId, false, err => { - expect(err).to.equal(null); - return done(); - }); - }); - - describe('when the container is already gone', function() { - beforeEach(function() { - this.fakeError = new Error('woops'); - this.fakeError.statusCode = 404; - this.fakeContainer = - {remove: sinon.stub().callsArgWith(1, this.fakeError)}; - return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); - }); - - return it('should not produce an error', function(done) { - return this.DockerRunner._destroyContainer(this.containerId, false, err => { - expect(err).to.equal(null); - return done(); - }); - }); - }); - - return describe('when container.destroy produces an error', function(done) { - beforeEach(function() { - this.fakeError = new Error('woops'); - this.fakeError.statusCode = 500; - this.fakeContainer = - {remove: sinon.stub().callsArgWith(1, this.fakeError)}; - return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); - }); - - return it('should produce an error', function(done) { - return this.DockerRunner._destroyContainer(this.containerId, false, err => { - expect(err).to.not.equal(null); - expect(err).to.equal(this.fakeError); - return done(); - }); - }); - }); - }); - - - return describe('kill', function() { - beforeEach(function() { - this.containerId = 'some_id'; - this.fakeContainer = - {kill: sinon.stub().callsArgWith(0, null)}; - return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); - }); - - it('should get the container', function(done) { - return this.DockerRunner.kill(this.containerId, err => { - this.Docker.prototype.getContainer.callCount.should.equal(1); - this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true); - return done(); - }); - }); - - it('should try to force-destroy the container', function(done) { - return this.DockerRunner.kill(this.containerId, err => { - this.fakeContainer.kill.callCount.should.equal(1); - return done(); - }); - }); - - it('should not produce an error', function(done) { - return this.DockerRunner.kill(this.containerId, err => { - expect(err).to.equal(undefined); - return done(); - }); - }); - - describe('when the container is not actually running', function() { - beforeEach(function() { - this.fakeError = new Error('woops'); - this.fakeError.statusCode = 500; - this.fakeError.message = 'Cannot kill container <whatever> is not running'; - this.fakeContainer = - {kill: sinon.stub().callsArgWith(0, this.fakeError)}; - return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); - }); - - return it('should not produce an error', function(done) { - return this.DockerRunner.kill(this.containerId, err => { - expect(err).to.equal(undefined); - return done(); - }); - }); - }); - - return describe('when container.kill produces a legitimate error', function(done) { - beforeEach(function() { - this.fakeError = new Error('woops'); - this.fakeError.statusCode = 500; - this.fakeError.message = 'Totally legitimate reason to throw an error'; - this.fakeContainer = - {kill: sinon.stub().callsArgWith(0, this.fakeError)}; - return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer); - }); - - return it('should produce an error', function(done) { - return this.DockerRunner.kill(this.containerId, err => { - expect(err).to.not.equal(undefined); - expect(err).to.equal(this.fakeError); - return done(); - }); - }); - }); - }); -}); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const { expect } = require('chai') +require('coffee-script') +const modulePath = require('path').join( + __dirname, + '../../../app/coffee/DockerRunner' +) +const Path = require('path') + +describe('DockerRunner', function() { + beforeEach(function() { + let container, Docker, Timer + this.container = container = {} + this.DockerRunner = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.Settings = { + clsi: { docker: {} }, + path: {} + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + info: sinon.stub(), + warn: sinon.stub() + }), + dockerode: (Docker = (function() { + Docker = class Docker { + static initClass() { + this.prototype.getContainer = sinon.stub().returns(container) + this.prototype.createContainer = sinon + .stub() + .yields(null, container) + this.prototype.listContainers = sinon.stub() + } + } + Docker.initClass() + return Docker + })()), + fs: (this.fs = { + stat: sinon.stub().yields(null, { + isDirectory() { + return true + } + }) + }), + './Metrics': { + Timer: (Timer = class Timer { + done() {} + }) + }, + './LockManager': { + runWithLock(key, runner, callback) { + return runner(callback) + } + } + } + }) + this.Docker = Docker + this.getContainer = Docker.prototype.getContainer + this.createContainer = Docker.prototype.createContainer + this.listContainers = Docker.prototype.listContainers + + this.directory = '/local/compile/directory' + this.mainFile = 'main-file.tex' + this.compiler = 'pdflatex' + this.image = 'example.com/sharelatex/image:2016.2' + this.env = {} + this.callback = sinon.stub() + this.project_id = 'project-id-123' + this.volumes = { '/local/compile/directory': '/compile' } + this.Settings.clsi.docker.image = this.defaultImage = 'default-image' + return (this.Settings.clsi.docker.env = { PATH: 'mock-path' }) + }) + + describe('run', function() { + beforeEach(function(done) { + this.DockerRunner._getContainerOptions = sinon + .stub() + .returns((this.options = { mockoptions: 'foo' })) + this.DockerRunner._fingerprintContainer = sinon + .stub() + .returns((this.fingerprint = 'fingerprint')) + + this.name = `project-${this.project_id}-${this.fingerprint}` + + this.command = ['mock', 'command', '--outdir=$COMPILE_DIR'] + this.command_with_dir = ['mock', 'command', '--outdir=/compile'] + this.timeout = 42000 + return done() + }) + + describe('successfully', function() { + beforeEach(function(done) { + this.DockerRunner._runAndWaitForContainer = sinon + .stub() + .callsArgWith(3, null, (this.output = 'mock-output')) + return this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + this.image, + this.timeout, + this.env, + (err, output) => { + this.callback(err, output) + return done() + } + ) + }) + + it('should generate the options for the container', function() { + return this.DockerRunner._getContainerOptions + .calledWith( + this.command_with_dir, + this.image, + this.volumes, + this.timeout + ) + .should.equal(true) + }) + + it('should generate the fingerprint from the returned options', function() { + return this.DockerRunner._fingerprintContainer + .calledWith(this.options) + .should.equal(true) + }) + + it('should do the run', function() { + return this.DockerRunner._runAndWaitForContainer + .calledWith(this.options, this.volumes, this.timeout) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null, this.output).should.equal(true) + }) + }) + + describe('when path.sandboxedCompilesHostDir is set', function() { + beforeEach(function() { + this.Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles' + this.directory = '/var/lib/sharelatex/data/compiles/xyz' + this.DockerRunner._runAndWaitForContainer = sinon + .stub() + .callsArgWith(3, null, (this.output = 'mock-output')) + return this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + this.image, + this.timeout, + this.env, + this.callback + ) + }) + + it('should re-write the bind directory', function() { + const volumes = this.DockerRunner._runAndWaitForContainer.lastCall + .args[1] + return expect(volumes).to.deep.equal({ + '/some/host/dir/compiles/xyz': '/compile' + }) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null, this.output).should.equal(true) + }) + }) + + describe('when the run throws an error', function() { + beforeEach(function() { + let firstTime = true + this.output = 'mock-output' + this.DockerRunner._runAndWaitForContainer = ( + options, + volumes, + timeout, + callback + ) => { + if (callback == null) { + callback = function(error, output) {} + } + if (firstTime) { + firstTime = false + return callback( + new Error('HTTP code is 500 which indicates error: server error') + ) + } else { + return callback(null, this.output) + } + } + sinon.spy(this.DockerRunner, '_runAndWaitForContainer') + this.DockerRunner.destroyContainer = sinon.stub().callsArg(3) + return this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + this.image, + this.timeout, + this.env, + this.callback + ) + }) + + it('should do the run twice', function() { + return this.DockerRunner._runAndWaitForContainer.calledTwice.should.equal( + true + ) + }) + + it('should destroy the container in between', function() { + return this.DockerRunner.destroyContainer + .calledWith(this.name, null) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null, this.output).should.equal(true) + }) + }) + + describe('with no image', function() { + beforeEach(function() { + this.DockerRunner._runAndWaitForContainer = sinon + .stub() + .callsArgWith(3, null, (this.output = 'mock-output')) + return this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + null, + this.timeout, + this.env, + this.callback + ) + }) + + return it('should use the default image', function() { + return this.DockerRunner._getContainerOptions + .calledWith( + this.command_with_dir, + this.defaultImage, + this.volumes, + this.timeout + ) + .should.equal(true) + }) + }) + + return describe('with image override', function() { + beforeEach(function() { + this.Settings.texliveImageNameOveride = 'overrideimage.com/something' + this.DockerRunner._runAndWaitForContainer = sinon + .stub() + .callsArgWith(3, null, (this.output = 'mock-output')) + return this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + this.image, + this.timeout, + this.env, + this.callback + ) + }) + + return it('should use the override and keep the tag', function() { + const image = this.DockerRunner._getContainerOptions.args[0][1] + return image.should.equal('overrideimage.com/something/image:2016.2') + }) + }) + }) + + describe('_runAndWaitForContainer', function() { + beforeEach(function() { + this.options = { mockoptions: 'foo', name: (this.name = 'mock-name') } + this.DockerRunner.startContainer = ( + options, + volumes, + attachStreamHandler, + callback + ) => { + attachStreamHandler(null, (this.output = 'mock-output')) + return callback(null, (this.containerId = 'container-id')) + } + sinon.spy(this.DockerRunner, 'startContainer') + this.DockerRunner.waitForContainer = sinon + .stub() + .callsArgWith(2, null, (this.exitCode = 42)) + return this.DockerRunner._runAndWaitForContainer( + this.options, + this.volumes, + this.timeout, + this.callback + ) + }) + + it('should create/start the container', function() { + return this.DockerRunner.startContainer + .calledWith(this.options, this.volumes) + .should.equal(true) + }) + + it('should wait for the container to finish', function() { + return this.DockerRunner.waitForContainer + .calledWith(this.name, this.timeout) + .should.equal(true) + }) + + return it('should call the callback with the output', function() { + return this.callback.calledWith(null, this.output).should.equal(true) + }) + }) + + describe('startContainer', function() { + beforeEach(function() { + this.attachStreamHandler = sinon.stub() + this.attachStreamHandler.cock = true + this.options = { mockoptions: 'foo', name: 'mock-name' } + this.container.inspect = sinon.stub().callsArgWith(0) + this.DockerRunner.attachToContainer = ( + containerId, + attachStreamHandler, + cb + ) => { + attachStreamHandler() + return cb() + } + return sinon.spy(this.DockerRunner, 'attachToContainer') + }) + + describe('when the container exists', function() { + beforeEach(function() { + this.container.inspect = sinon.stub().callsArgWith(0) + this.container.start = sinon.stub().yields() + + return this.DockerRunner.startContainer( + this.options, + this.volumes, + this.callback, + () => {} + ) + }) + + it('should start the container with the given name', function() { + this.getContainer.calledWith(this.options.name).should.equal(true) + return this.container.start.called.should.equal(true) + }) + + it('should not try to create the container', function() { + return this.createContainer.called.should.equal(false) + }) + + it('should attach to the container', function() { + return this.DockerRunner.attachToContainer.called.should.equal(true) + }) + + it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + + return it('should attach before the container starts', function() { + return sinon.assert.callOrder( + this.DockerRunner.attachToContainer, + this.container.start + ) + }) + }) + + describe('when the container does not exist', function() { + beforeEach(function() { + const exists = false + this.container.start = sinon.stub().yields() + this.container.inspect = sinon + .stub() + .callsArgWith(0, { statusCode: 404 }) + return this.DockerRunner.startContainer( + this.options, + this.volumes, + this.attachStreamHandler, + this.callback + ) + }) + + it('should create the container', function() { + return this.createContainer.calledWith(this.options).should.equal(true) + }) + + it('should call the callback and stream handler', function() { + this.attachStreamHandler.called.should.equal(true) + return this.callback.called.should.equal(true) + }) + + it('should attach to the container', function() { + return this.DockerRunner.attachToContainer.called.should.equal(true) + }) + + return it('should attach before the container starts', function() { + return sinon.assert.callOrder( + this.DockerRunner.attachToContainer, + this.container.start + ) + }) + }) + + describe('when the container is already running', function() { + beforeEach(function() { + const error = new Error( + `HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.` + ) + error.statusCode = 304 + this.container.start = sinon.stub().yields(error) + this.container.inspect = sinon.stub().callsArgWith(0) + return this.DockerRunner.startContainer( + this.options, + this.volumes, + this.attachStreamHandler, + this.callback + ) + }) + + it('should not try to create the container', function() { + return this.createContainer.called.should.equal(false) + }) + + return it('should call the callback and stream handler without an error', function() { + this.attachStreamHandler.called.should.equal(true) + return this.callback.called.should.equal(true) + }) + }) + + describe('when a volume does not exist', function() { + beforeEach(function() { + this.fs.stat = sinon.stub().yields(new Error('no such path')) + return this.DockerRunner.startContainer( + this.options, + this.volumes, + this.attachStreamHandler, + this.callback + ) + }) + + it('should not try to create the container', function() { + return this.createContainer.called.should.equal(false) + }) + + return it('should call the callback with an error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + + describe('when a volume exists but is not a directory', function() { + beforeEach(function() { + this.fs.stat = sinon.stub().yields(null, { + isDirectory() { + return false + } + }) + return this.DockerRunner.startContainer( + this.options, + this.volumes, + this.attachStreamHandler, + this.callback + ) + }) + + it('should not try to create the container', function() { + return this.createContainer.called.should.equal(false) + }) + + return it('should call the callback with an error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + + describe('when a volume does not exist, but sibling-containers are used', function() { + beforeEach(function() { + this.fs.stat = sinon.stub().yields(new Error('no such path')) + this.Settings.path.sandboxedCompilesHostDir = '/some/path' + this.container.start = sinon.stub().yields() + return this.DockerRunner.startContainer( + this.options, + this.volumes, + this.callback + ) + }) + + afterEach(function() { + return delete this.Settings.path.sandboxedCompilesHostDir + }) + + it('should start the container with the given name', function() { + this.getContainer.calledWith(this.options.name).should.equal(true) + return this.container.start.called.should.equal(true) + }) + + it('should not try to create the container', function() { + return this.createContainer.called.should.equal(false) + }) + + return it('should call the callback', function() { + this.callback.called.should.equal(true) + return this.callback.calledWith(new Error()).should.equal(false) + }) + }) + + return describe('when the container tries to be created, but already has been (race condition)', function() {}) + }) + + describe('waitForContainer', function() { + beforeEach(function() { + this.containerId = 'container-id' + this.timeout = 5000 + this.container.wait = sinon + .stub() + .yields(null, { StatusCode: (this.statusCode = 42) }) + return (this.container.kill = sinon.stub().yields()) + }) + + describe('when the container returns in time', function() { + beforeEach(function() { + return this.DockerRunner.waitForContainer( + this.containerId, + this.timeout, + this.callback + ) + }) + + it('should wait for the container', function() { + this.getContainer.calledWith(this.containerId).should.equal(true) + return this.container.wait.called.should.equal(true) + }) + + return it('should call the callback with the exit', function() { + return this.callback + .calledWith(null, this.statusCode) + .should.equal(true) + }) + }) + + return describe('when the container does not return before the timeout', function() { + beforeEach(function(done) { + this.container.wait = function(callback) { + if (callback == null) { + callback = function(error, exitCode) {} + } + return setTimeout(() => callback(null, { StatusCode: 42 }), 100) + } + this.timeout = 5 + return this.DockerRunner.waitForContainer( + this.containerId, + this.timeout, + (...args) => { + this.callback(...Array.from(args || [])) + return done() + } + ) + }) + + it('should call kill on the container', function() { + this.getContainer.calledWith(this.containerId).should.equal(true) + return this.container.kill.called.should.equal(true) + }) + + return it('should call the callback with an error', function() { + const error = new Error('container timed out') + error.timedout = true + return this.callback.calledWith(error).should.equal(true) + }) + }) + }) + + describe('destroyOldContainers', function() { + beforeEach(function(done) { + const oneHourInSeconds = 60 * 60 + const oneHourInMilliseconds = oneHourInSeconds * 1000 + const nowInSeconds = Date.now() / 1000 + this.containers = [ + { + Name: '/project-old-container-name', + Id: 'old-container-id', + Created: nowInSeconds - oneHourInSeconds - 100 + }, + { + Name: '/project-new-container-name', + Id: 'new-container-id', + Created: nowInSeconds - oneHourInSeconds + 100 + }, + { + Name: '/totally-not-a-project-container', + Id: 'some-random-id', + Created: nowInSeconds - 2 * oneHourInSeconds + } + ] + this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds + this.listContainers.callsArgWith(1, null, this.containers) + this.DockerRunner.destroyContainer = sinon.stub().callsArg(3) + return this.DockerRunner.destroyOldContainers(error => { + this.callback(error) + return done() + }) + }) + + it('should list all containers', function() { + return this.listContainers.calledWith({ all: true }).should.equal(true) + }) + + it('should destroy old containers', function() { + this.DockerRunner.destroyContainer.callCount.should.equal(1) + return this.DockerRunner.destroyContainer + .calledWith('/project-old-container-name', 'old-container-id') + .should.equal(true) + }) + + it('should not destroy new containers', function() { + return this.DockerRunner.destroyContainer + .calledWith('/project-new-container-name', 'new-container-id') + .should.equal(false) + }) + + it('should not destroy non-project containers', function() { + return this.DockerRunner.destroyContainer + .calledWith('/totally-not-a-project-container', 'some-random-id') + .should.equal(false) + }) + + return it('should callback the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('_destroyContainer', function() { + beforeEach(function() { + this.containerId = 'some_id' + this.fakeContainer = { remove: sinon.stub().callsArgWith(1, null) } + return (this.Docker.prototype.getContainer = sinon + .stub() + .returns(this.fakeContainer)) + }) + + it('should get the container', function(done) { + return this.DockerRunner._destroyContainer( + this.containerId, + false, + err => { + this.Docker.prototype.getContainer.callCount.should.equal(1) + this.Docker.prototype.getContainer + .calledWith(this.containerId) + .should.equal(true) + return done() + } + ) + }) + + it('should try to force-destroy the container when shouldForce=true', function(done) { + return this.DockerRunner._destroyContainer( + this.containerId, + true, + err => { + this.fakeContainer.remove.callCount.should.equal(1) + this.fakeContainer.remove + .calledWith({ force: true }) + .should.equal(true) + return done() + } + ) + }) + + it('should not try to force-destroy the container when shouldForce=false', function(done) { + return this.DockerRunner._destroyContainer( + this.containerId, + false, + err => { + this.fakeContainer.remove.callCount.should.equal(1) + this.fakeContainer.remove + .calledWith({ force: false }) + .should.equal(true) + return done() + } + ) + }) + + it('should not produce an error', function(done) { + return this.DockerRunner._destroyContainer( + this.containerId, + false, + err => { + expect(err).to.equal(null) + return done() + } + ) + }) + + describe('when the container is already gone', function() { + beforeEach(function() { + this.fakeError = new Error('woops') + this.fakeError.statusCode = 404 + this.fakeContainer = { + remove: sinon.stub().callsArgWith(1, this.fakeError) + } + return (this.Docker.prototype.getContainer = sinon + .stub() + .returns(this.fakeContainer)) + }) + + return it('should not produce an error', function(done) { + return this.DockerRunner._destroyContainer( + this.containerId, + false, + err => { + expect(err).to.equal(null) + return done() + } + ) + }) + }) + + return describe('when container.destroy produces an error', function(done) { + beforeEach(function() { + this.fakeError = new Error('woops') + this.fakeError.statusCode = 500 + this.fakeContainer = { + remove: sinon.stub().callsArgWith(1, this.fakeError) + } + return (this.Docker.prototype.getContainer = sinon + .stub() + .returns(this.fakeContainer)) + }) + + return it('should produce an error', function(done) { + return this.DockerRunner._destroyContainer( + this.containerId, + false, + err => { + expect(err).to.not.equal(null) + expect(err).to.equal(this.fakeError) + return done() + } + ) + }) + }) + }) + + return describe('kill', function() { + beforeEach(function() { + this.containerId = 'some_id' + this.fakeContainer = { kill: sinon.stub().callsArgWith(0, null) } + return (this.Docker.prototype.getContainer = sinon + .stub() + .returns(this.fakeContainer)) + }) + + it('should get the container', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + this.Docker.prototype.getContainer.callCount.should.equal(1) + this.Docker.prototype.getContainer + .calledWith(this.containerId) + .should.equal(true) + return done() + }) + }) + + it('should try to force-destroy the container', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + this.fakeContainer.kill.callCount.should.equal(1) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + expect(err).to.equal(undefined) + return done() + }) + }) + + describe('when the container is not actually running', function() { + beforeEach(function() { + this.fakeError = new Error('woops') + this.fakeError.statusCode = 500 + this.fakeError.message = + 'Cannot kill container <whatever> is not running' + this.fakeContainer = { + kill: sinon.stub().callsArgWith(0, this.fakeError) + } + return (this.Docker.prototype.getContainer = sinon + .stub() + .returns(this.fakeContainer)) + }) + + return it('should not produce an error', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + expect(err).to.equal(undefined) + return done() + }) + }) + }) + + return describe('when container.kill produces a legitimate error', function(done) { + beforeEach(function() { + this.fakeError = new Error('woops') + this.fakeError.statusCode = 500 + this.fakeError.message = 'Totally legitimate reason to throw an error' + this.fakeContainer = { + kill: sinon.stub().callsArgWith(0, this.fakeError) + } + return (this.Docker.prototype.getContainer = sinon + .stub() + .returns(this.fakeContainer)) + }) + + return it('should produce an error', function(done) { + return this.DockerRunner.kill(this.containerId, err => { + expect(err).to.not.equal(undefined) + expect(err).to.equal(this.fakeError) + return done() + }) + }) + }) + }) +}) diff --git a/test/unit/js/DraftModeManagerTests.js b/test/unit/js/DraftModeManagerTests.js index f270873a..2c30b404 100644 --- a/test/unit/js/DraftModeManagerTests.js +++ b/test/unit/js/DraftModeManagerTests.js @@ -8,75 +8,79 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/DraftModeManager'); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/DraftModeManager' +) describe('DraftModeManager', function() { - beforeEach(function() { - return this.DraftModeManager = SandboxedModule.require(modulePath, { requires: { - "fs": (this.fs = {}), - "logger-sharelatex": (this.logger = {log() {}}) - } - });}); - - describe("_injectDraftOption", function() { - it("should add draft option into documentclass with existing options", function() { - return this.DraftModeManager - ._injectDraftOption(`\ + beforeEach(function() { + return (this.DraftModeManager = SandboxedModule.require(modulePath, { + requires: { + fs: (this.fs = {}), + 'logger-sharelatex': (this.logger = { log() {} }) + } + })) + }) + + describe('_injectDraftOption', function() { + it('should add draft option into documentclass with existing options', function() { + return this.DraftModeManager._injectDraftOption(`\ \\documentclass[a4paper,foo=bar]{article}\ -`) - .should.equal(`\ +`).should.equal(`\ \\documentclass[draft,a4paper,foo=bar]{article}\ -`); - }); +`) + }) - return it("should add draft option into documentclass with no options", function() { - return this.DraftModeManager - ._injectDraftOption(`\ + return it('should add draft option into documentclass with no options', function() { + return this.DraftModeManager._injectDraftOption(`\ \\documentclass{article}\ -`) - .should.equal(`\ +`).should.equal(`\ \\documentclass[draft]{article}\ -`); - }); - }); - - return describe("injectDraftMode", function() { - beforeEach(function() { - this.filename = "/mock/filename.tex"; - this.callback = sinon.stub(); - const content = `\ +`) + }) + }) + + return describe('injectDraftMode', function() { + beforeEach(function() { + this.filename = '/mock/filename.tex' + this.callback = sinon.stub() + const content = `\ \\documentclass{article} \\begin{document} Hello world \\end{document}\ -`; - this.fs.readFile = sinon.stub().callsArgWith(2, null, content); - this.fs.writeFile = sinon.stub().callsArg(2); - return this.DraftModeManager.injectDraftMode(this.filename, this.callback); - }); - - it("should read the file", function() { - return this.fs.readFile - .calledWith(this.filename, "utf8") - .should.equal(true); - }); - - it("should write the modified file", function() { - return this.fs.writeFile - .calledWith(this.filename, `\ +` + this.fs.readFile = sinon.stub().callsArgWith(2, null, content) + this.fs.writeFile = sinon.stub().callsArg(2) + return this.DraftModeManager.injectDraftMode(this.filename, this.callback) + }) + + it('should read the file', function() { + return this.fs.readFile + .calledWith(this.filename, 'utf8') + .should.equal(true) + }) + + it('should write the modified file', function() { + return this.fs.writeFile + .calledWith( + this.filename, + `\ \\documentclass[draft]{article} \\begin{document} Hello world \\end{document}\ -`) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); -}); +` + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index 7fe8bc81..b468b831 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -9,103 +9,129 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/LatexRunner'); -const Path = require("path"); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/LatexRunner' +) +const Path = require('path') -describe("LatexRunner", function() { - beforeEach(function() { - let Timer; - this.LatexRunner = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.Settings = { - docker: { - socketPath: "/var/run/docker.sock" - } - }), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./Metrics": { - Timer: (Timer = class Timer { - done() {} - }) - }, - "./CommandRunner": (this.CommandRunner = {}) - } - }); +describe('LatexRunner', function() { + beforeEach(function() { + let Timer + this.LatexRunner = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.Settings = { + docker: { + socketPath: '/var/run/docker.sock' + } + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './Metrics': { + Timer: (Timer = class Timer { + done() {} + }) + }, + './CommandRunner': (this.CommandRunner = {}) + } + }) - this.directory = "/local/compile/directory"; - this.mainFile = "main-file.tex"; - this.compiler = "pdflatex"; - this.image = "example.com/image"; - this.callback = sinon.stub(); - this.project_id = "project-id-123"; - return this.env = {'foo': '123'};}); + this.directory = '/local/compile/directory' + this.mainFile = 'main-file.tex' + this.compiler = 'pdflatex' + this.image = 'example.com/image' + this.callback = sinon.stub() + this.project_id = 'project-id-123' + return (this.env = { foo: '123' }) + }) - return describe("runLatex", function() { - beforeEach(function() { - return this.CommandRunner.run = sinon.stub().callsArg(6); - }); + return describe('runLatex', function() { + beforeEach(function() { + return (this.CommandRunner.run = sinon.stub().callsArg(6)) + }) - describe("normally", function() { - beforeEach(function() { - return this.LatexRunner.runLatex(this.project_id, { - directory: this.directory, - mainFile: this.mainFile, - compiler: this.compiler, - timeout: (this.timeout = 42000), - image: this.image, - environment: this.env - }, - this.callback); - }); + describe('normally', function() { + beforeEach(function() { + return this.LatexRunner.runLatex( + this.project_id, + { + directory: this.directory, + mainFile: this.mainFile, + compiler: this.compiler, + timeout: (this.timeout = 42000), + image: this.image, + environment: this.env + }, + this.callback + ) + }) - return it("should run the latex command", function() { - return this.CommandRunner.run - .calledWith(this.project_id, sinon.match.any, this.directory, this.image, this.timeout, this.env) - .should.equal(true); - }); - }); + return it('should run the latex command', function() { + return this.CommandRunner.run + .calledWith( + this.project_id, + sinon.match.any, + this.directory, + this.image, + this.timeout, + this.env + ) + .should.equal(true) + }) + }) - describe("with an .Rtex main file", function() { - beforeEach(function() { - return this.LatexRunner.runLatex(this.project_id, { - directory: this.directory, - mainFile: "main-file.Rtex", - compiler: this.compiler, - image: this.image, - timeout: (this.timeout = 42000) - }, - this.callback); - }); + describe('with an .Rtex main file', function() { + beforeEach(function() { + return this.LatexRunner.runLatex( + this.project_id, + { + directory: this.directory, + mainFile: 'main-file.Rtex', + compiler: this.compiler, + image: this.image, + timeout: (this.timeout = 42000) + }, + this.callback + ) + }) - return it("should run the latex command on the equivalent .tex file", function() { - const command = this.CommandRunner.run.args[0][1]; - const mainFile = command.slice(-1)[0]; - return mainFile.should.equal("$COMPILE_DIR/main-file.tex"); - }); - }); + return it('should run the latex command on the equivalent .tex file', function() { + const command = this.CommandRunner.run.args[0][1] + const mainFile = command.slice(-1)[0] + return mainFile.should.equal('$COMPILE_DIR/main-file.tex') + }) + }) - return describe("with a flags option", function() { - beforeEach(function() { - return this.LatexRunner.runLatex(this.project_id, { - directory: this.directory, - mainFile: this.mainFile, - compiler: this.compiler, - image: this.image, - timeout: (this.timeout = 42000), - flags: ["-file-line-error", "-halt-on-error"] - }, - this.callback); - }); + return describe('with a flags option', function() { + beforeEach(function() { + return this.LatexRunner.runLatex( + this.project_id, + { + directory: this.directory, + mainFile: this.mainFile, + compiler: this.compiler, + image: this.image, + timeout: (this.timeout = 42000), + flags: ['-file-line-error', '-halt-on-error'] + }, + this.callback + ) + }) - return it("should include the flags in the command", function() { - const command = this.CommandRunner.run.args[0][1]; - const flags = command.filter(arg => (arg === "-file-line-error") || (arg === "-halt-on-error")); - flags.length.should.equal(2); - flags[0].should.equal("-file-line-error"); - return flags[1].should.equal("-halt-on-error"); - }); - }); - }); -}); + return it('should include the flags in the command', function() { + const command = this.CommandRunner.run.args[0][1] + const flags = command.filter( + arg => arg === '-file-line-error' || arg === '-halt-on-error' + ) + flags.length.should.equal(2) + flags[0].should.equal('-file-line-error') + return flags[1].should.equal('-halt-on-error') + }) + }) + }) +}) diff --git a/test/unit/js/LockManagerTests.js b/test/unit/js/LockManagerTests.js index 6d1b1562..ea6c3419 100644 --- a/test/unit/js/LockManagerTests.js +++ b/test/unit/js/LockManagerTests.js @@ -9,75 +9,85 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/LockManager'); -const Path = require("path"); -const Errors = require("../../../app/js/Errors"); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/LockManager' +) +const Path = require('path') +const Errors = require('../../../app/js/Errors') -describe("DockerLockManager", function() { - beforeEach(function() { - this.LockManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": {}, - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), err() {} }), - "fs": { - lstat:sinon.stub().callsArgWith(1), - readdir: sinon.stub().callsArgWith(1) - }, - "lockfile": (this.Lockfile = {}) - } - }); - return this.lockFile = "/local/compile/directory/.project-lock"; - }); +describe('DockerLockManager', function() { + beforeEach(function() { + this.LockManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': {}, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }), + fs: { + lstat: sinon.stub().callsArgWith(1), + readdir: sinon.stub().callsArgWith(1) + }, + lockfile: (this.Lockfile = {}) + } + }) + return (this.lockFile = '/local/compile/directory/.project-lock') + }) - return describe("runWithLock", function() { - beforeEach(function() { - this.runner = sinon.stub().callsArgWith(0, null, "foo", "bar"); - return this.callback = sinon.stub(); - }); + return describe('runWithLock', function() { + beforeEach(function() { + this.runner = sinon.stub().callsArgWith(0, null, 'foo', 'bar') + return (this.callback = sinon.stub()) + }) - describe("normally", function() { - beforeEach(function() { - this.Lockfile.lock = sinon.stub().callsArgWith(2, null); - this.Lockfile.unlock = sinon.stub().callsArgWith(1, null); - return this.LockManager.runWithLock(this.lockFile, this.runner, this.callback); - }); + describe('normally', function() { + beforeEach(function() { + this.Lockfile.lock = sinon.stub().callsArgWith(2, null) + this.Lockfile.unlock = sinon.stub().callsArgWith(1, null) + return this.LockManager.runWithLock( + this.lockFile, + this.runner, + this.callback + ) + }) - it("should run the compile", function() { - return this.runner - .calledWith() - .should.equal(true); - }); + it('should run the compile', function() { + return this.runner.calledWith().should.equal(true) + }) - return it("should call the callback with the response from the compile", function() { - return this.callback - .calledWithExactly(null, "foo", "bar") - .should.equal(true); - }); - }); + return it('should call the callback with the response from the compile', function() { + return this.callback + .calledWithExactly(null, 'foo', 'bar') + .should.equal(true) + }) + }) - return describe("when the project is locked", function() { - beforeEach(function() { - this.error = new Error(); - this.error.code = "EEXIST"; - this.Lockfile.lock = sinon.stub().callsArgWith(2,this.error); - this.Lockfile.unlock = sinon.stub().callsArgWith(1, null); - return this.LockManager.runWithLock(this.lockFile, this.runner, this.callback); - }); + return describe('when the project is locked', function() { + beforeEach(function() { + this.error = new Error() + this.error.code = 'EEXIST' + this.Lockfile.lock = sinon.stub().callsArgWith(2, this.error) + this.Lockfile.unlock = sinon.stub().callsArgWith(1, null) + return this.LockManager.runWithLock( + this.lockFile, + this.runner, + this.callback + ) + }) - it("should not run the compile", function() { - return this.runner - .called - .should.equal(false); - }); + it('should not run the compile', function() { + return this.runner.called.should.equal(false) + }) - return it("should return an error", function() { - const error = new Errors.AlreadyCompilingError(); - return this.callback - .calledWithExactly(error) - .should.equal(true); - }); - }); - }); -}); + return it('should return an error', function() { + const error = new Errors.AlreadyCompilingError() + return this.callback.calledWithExactly(error).should.equal(true) + }) + }) + }) +}) diff --git a/test/unit/js/OutputFileFinderTests.js b/test/unit/js/OutputFileFinderTests.js index 5c956adb..e5f99046 100644 --- a/test/unit/js/OutputFileFinderTests.js +++ b/test/unit/js/OutputFileFinderTests.js @@ -10,90 +10,96 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/OutputFileFinder'); -const path = require("path"); -const { expect } = require("chai"); -const { EventEmitter } = require("events"); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/OutputFileFinder' +) +const path = require('path') +const { expect } = require('chai') +const { EventEmitter } = require('events') -describe("OutputFileFinder", function() { - beforeEach(function() { - this.OutputFileFinder = SandboxedModule.require(modulePath, { requires: { - "fs": (this.fs = {}), - "child_process": { spawn: (this.spawn = sinon.stub()) - }, - "logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() } - } - }); - this.directory = "/test/dir"; - return this.callback = sinon.stub(); - }); +describe('OutputFileFinder', function() { + beforeEach(function() { + this.OutputFileFinder = SandboxedModule.require(modulePath, { + requires: { + fs: (this.fs = {}), + child_process: { spawn: (this.spawn = sinon.stub()) }, + 'logger-sharelatex': { log: sinon.stub(), warn: sinon.stub() } + } + }) + this.directory = '/test/dir' + return (this.callback = sinon.stub()) + }) - describe("findOutputFiles", function() { - beforeEach(function() { - this.resource_path = "resource/path.tex"; - this.output_paths = ["output.pdf", "extra/file.tex"]; - this.all_paths = this.output_paths.concat([this.resource_path]); - this.resources = [ - {path: (this.resource_path = "resource/path.tex")} - ]; - this.OutputFileFinder._getAllFiles = sinon.stub().callsArgWith(1, null, this.all_paths); - return this.OutputFileFinder.findOutputFiles(this.resources, this.directory, (error, outputFiles) => { - this.outputFiles = outputFiles; - - }); - }); + describe('findOutputFiles', function() { + beforeEach(function() { + this.resource_path = 'resource/path.tex' + this.output_paths = ['output.pdf', 'extra/file.tex'] + this.all_paths = this.output_paths.concat([this.resource_path]) + this.resources = [{ path: (this.resource_path = 'resource/path.tex') }] + this.OutputFileFinder._getAllFiles = sinon + .stub() + .callsArgWith(1, null, this.all_paths) + return this.OutputFileFinder.findOutputFiles( + this.resources, + this.directory, + (error, outputFiles) => { + this.outputFiles = outputFiles + } + ) + }) - return it("should only return the output files, not directories or resource paths", function() { - return expect(this.outputFiles).to.deep.equal([{ - path: "output.pdf", - type: "pdf" - }, { - path: "extra/file.tex", - type: "tex" - }]); - }); -}); - - return describe("_getAllFiles", function() { - beforeEach(function() { - this.proc = new EventEmitter(); - this.proc.stdout = new EventEmitter(); - this.spawn.returns(this.proc); - this.directory = "/base/dir"; - return this.OutputFileFinder._getAllFiles(this.directory, this.callback); - }); - - describe("successfully", function() { - beforeEach(function() { - this.proc.stdout.emit( - "data", - ["/base/dir/main.tex", "/base/dir/chapters/chapter1.tex"].join("\n") + "\n" - ); - return this.proc.emit("close", 0); - }); - - return it("should call the callback with the relative file paths", function() { - return this.callback.calledWith( - null, - ["main.tex", "chapters/chapter1.tex"] - ).should.equal(true); - }); - }); + return it('should only return the output files, not directories or resource paths', function() { + return expect(this.outputFiles).to.deep.equal([ + { + path: 'output.pdf', + type: 'pdf' + }, + { + path: 'extra/file.tex', + type: 'tex' + } + ]) + }) + }) - return describe("when the directory doesn't exist", function() { - beforeEach(function() { - return this.proc.emit("close", 1); - }); - - return it("should call the callback with a blank array", function() { - return this.callback.calledWith( - null, - [] - ).should.equal(true); - }); - }); - }); -}); + return describe('_getAllFiles', function() { + beforeEach(function() { + this.proc = new EventEmitter() + this.proc.stdout = new EventEmitter() + this.spawn.returns(this.proc) + this.directory = '/base/dir' + return this.OutputFileFinder._getAllFiles(this.directory, this.callback) + }) + + describe('successfully', function() { + beforeEach(function() { + this.proc.stdout.emit( + 'data', + ['/base/dir/main.tex', '/base/dir/chapters/chapter1.tex'].join('\n') + + '\n' + ) + return this.proc.emit('close', 0) + }) + + return it('should call the callback with the relative file paths', function() { + return this.callback + .calledWith(null, ['main.tex', 'chapters/chapter1.tex']) + .should.equal(true) + }) + }) + + return describe("when the directory doesn't exist", function() { + beforeEach(function() { + return this.proc.emit('close', 1) + }) + + return it('should call the callback with a blank array', function() { + return this.callback.calledWith(null, []).should.equal(true) + }) + }) + }) +}) diff --git a/test/unit/js/OutputFileOptimiserTests.js b/test/unit/js/OutputFileOptimiserTests.js index 13b8d60c..4546f084 100644 --- a/test/unit/js/OutputFileOptimiserTests.js +++ b/test/unit/js/OutputFileOptimiserTests.js @@ -10,139 +10,187 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/OutputFileOptimiser'); -const path = require("path"); -const { expect } = require("chai"); -const { EventEmitter } = require("events"); - -describe("OutputFileOptimiser", function() { - beforeEach(function() { - this.OutputFileOptimiser = SandboxedModule.require(modulePath, { requires: { - "fs": (this.fs = {}), - "path": (this.Path = {}), - "child_process": { spawn: (this.spawn = sinon.stub()) - }, - "logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() }, - "./Metrics" : {} - } - }); - this.directory = "/test/dir"; - return this.callback = sinon.stub(); - }); - - describe("optimiseFile", function() { - beforeEach(function() { - this.src = "./output.pdf"; - return this.dst = "./output.pdf"; - }); - - describe("when the file is not a pdf file", function() { - beforeEach(function(done){ - this.src = "./output.log"; - this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false); - this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null); - return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done); - }); - - it("should not check if the file is optimised", function() { - return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(false); - }); - - return it("should not optimise the file", function() { - return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(false); - }); - }); - - describe("when the pdf file is not optimised", function() { - beforeEach(function(done) { - this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false); - this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null); - return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done); - }); - - it("should check if the pdf is optimised", function() { - return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(true); - }); - - return it("should optimise the pdf", function() { - return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(true); - }); - }); - - return describe("when the pdf file is optimised", function() { - beforeEach(function(done) { - this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, true); - this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null); - return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done); - }); - - it("should check if the pdf is optimised", function() { - return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(true); - }); - - return it("should not optimise the pdf", function() { - return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(false); - }); - }); - }); - - return describe("checkIfPDFISOptimised", function() { - beforeEach(function() { - this.callback = sinon.stub(); - this.fd = 1234; - this.fs.open = sinon.stub().yields(null, this.fd); - this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello /Linearized 1")); - this.fs.close = sinon.stub().withArgs(this.fd).yields(null); - return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback); - }); - - describe("for a linearised file", function() { - beforeEach(function() { - this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello /Linearized 1")); - return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback); - }); - - it("should open the file", function() { - return this.fs.open.calledWith(this.src, "r").should.equal(true); - }); - - it("should read the header", function() { - return this.fs.read.calledWith(this.fd).should.equal(true); - }); - - it("should close the file", function() { - return this.fs.close.calledWith(this.fd).should.equal(true); - }); - - return it("should call the callback with a true result", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - - return describe("for an unlinearised file", function() { - beforeEach(function() { - this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello not linearized 1")); - return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback); - }); - - it("should open the file", function() { - return this.fs.open.calledWith(this.src, "r").should.equal(true); - }); - - it("should read the header", function() { - return this.fs.read.calledWith(this.fd).should.equal(true); - }); - - it("should close the file", function() { - return this.fs.close.calledWith(this.fd).should.equal(true); - }); - - return it("should call the callback with a false result", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); - }); -}); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/OutputFileOptimiser' +) +const path = require('path') +const { expect } = require('chai') +const { EventEmitter } = require('events') + +describe('OutputFileOptimiser', function() { + beforeEach(function() { + this.OutputFileOptimiser = SandboxedModule.require(modulePath, { + requires: { + fs: (this.fs = {}), + path: (this.Path = {}), + child_process: { spawn: (this.spawn = sinon.stub()) }, + 'logger-sharelatex': { log: sinon.stub(), warn: sinon.stub() }, + './Metrics': {} + } + }) + this.directory = '/test/dir' + return (this.callback = sinon.stub()) + }) + + describe('optimiseFile', function() { + beforeEach(function() { + this.src = './output.pdf' + return (this.dst = './output.pdf') + }) + + describe('when the file is not a pdf file', function() { + beforeEach(function(done) { + this.src = './output.log' + this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon + .stub() + .callsArgWith(1, null, false) + this.OutputFileOptimiser.optimisePDF = sinon + .stub() + .callsArgWith(2, null) + return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done) + }) + + it('should not check if the file is optimised', function() { + return this.OutputFileOptimiser.checkIfPDFIsOptimised + .calledWith(this.src) + .should.equal(false) + }) + + return it('should not optimise the file', function() { + return this.OutputFileOptimiser.optimisePDF + .calledWith(this.src, this.dst) + .should.equal(false) + }) + }) + + describe('when the pdf file is not optimised', function() { + beforeEach(function(done) { + this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon + .stub() + .callsArgWith(1, null, false) + this.OutputFileOptimiser.optimisePDF = sinon + .stub() + .callsArgWith(2, null) + return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done) + }) + + it('should check if the pdf is optimised', function() { + return this.OutputFileOptimiser.checkIfPDFIsOptimised + .calledWith(this.src) + .should.equal(true) + }) + + return it('should optimise the pdf', function() { + return this.OutputFileOptimiser.optimisePDF + .calledWith(this.src, this.dst) + .should.equal(true) + }) + }) + + return describe('when the pdf file is optimised', function() { + beforeEach(function(done) { + this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon + .stub() + .callsArgWith(1, null, true) + this.OutputFileOptimiser.optimisePDF = sinon + .stub() + .callsArgWith(2, null) + return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done) + }) + + it('should check if the pdf is optimised', function() { + return this.OutputFileOptimiser.checkIfPDFIsOptimised + .calledWith(this.src) + .should.equal(true) + }) + + return it('should not optimise the pdf', function() { + return this.OutputFileOptimiser.optimisePDF + .calledWith(this.src, this.dst) + .should.equal(false) + }) + }) + }) + + return describe('checkIfPDFISOptimised', function() { + beforeEach(function() { + this.callback = sinon.stub() + this.fd = 1234 + this.fs.open = sinon.stub().yields(null, this.fd) + this.fs.read = sinon + .stub() + .withArgs(this.fd) + .yields(null, 100, new Buffer('hello /Linearized 1')) + this.fs.close = sinon + .stub() + .withArgs(this.fd) + .yields(null) + return this.OutputFileOptimiser.checkIfPDFIsOptimised( + this.src, + this.callback + ) + }) + + describe('for a linearised file', function() { + beforeEach(function() { + this.fs.read = sinon + .stub() + .withArgs(this.fd) + .yields(null, 100, new Buffer('hello /Linearized 1')) + return this.OutputFileOptimiser.checkIfPDFIsOptimised( + this.src, + this.callback + ) + }) + + it('should open the file', function() { + return this.fs.open.calledWith(this.src, 'r').should.equal(true) + }) + + it('should read the header', function() { + return this.fs.read.calledWith(this.fd).should.equal(true) + }) + + it('should close the file', function() { + return this.fs.close.calledWith(this.fd).should.equal(true) + }) + + return it('should call the callback with a true result', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + return describe('for an unlinearised file', function() { + beforeEach(function() { + this.fs.read = sinon + .stub() + .withArgs(this.fd) + .yields(null, 100, new Buffer('hello not linearized 1')) + return this.OutputFileOptimiser.checkIfPDFIsOptimised( + this.src, + this.callback + ) + }) + + it('should open the file', function() { + return this.fs.open.calledWith(this.src, 'r').should.equal(true) + }) + + it('should read the header', function() { + return this.fs.read.calledWith(this.fd).should.equal(true) + }) + + it('should close the file', function() { + return this.fs.close.calledWith(this.fd).should.equal(true) + }) + + return it('should call the callback with a false result', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + }) +}) diff --git a/test/unit/js/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js index 5f77a807..0d84fc24 100644 --- a/test/unit/js/ProjectPersistenceManagerTests.js +++ b/test/unit/js/ProjectPersistenceManagerTests.js @@ -11,79 +11,90 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/ProjectPersistenceManager'); -const tk = require("timekeeper"); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/ProjectPersistenceManager' +) +const tk = require('timekeeper') -describe("ProjectPersistenceManager", function() { - beforeEach(function() { - this.ProjectPersistenceManager = SandboxedModule.require(modulePath, { requires: { - "./UrlCache": (this.UrlCache = {}), - "./CompileManager": (this.CompileManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub() }), - "./db": (this.db = {}) - } - }); - this.callback = sinon.stub(); - this.project_id = "project-id-123"; - return this.user_id = "1234"; - }); +describe('ProjectPersistenceManager', function() { + beforeEach(function() { + this.ProjectPersistenceManager = SandboxedModule.require(modulePath, { + requires: { + './UrlCache': (this.UrlCache = {}), + './CompileManager': (this.CompileManager = {}), + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + './db': (this.db = {}) + } + }) + this.callback = sinon.stub() + this.project_id = 'project-id-123' + return (this.user_id = '1234') + }) - describe("clearExpiredProjects", function() { - beforeEach(function() { - this.project_ids = [ - "project-id-1", - "project-id-2" - ]; - this.ProjectPersistenceManager._findExpiredProjectIds = sinon.stub().callsArgWith(0, null, this.project_ids); - this.ProjectPersistenceManager.clearProjectFromCache = sinon.stub().callsArg(1); - this.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1); - return this.ProjectPersistenceManager.clearExpiredProjects(this.callback); - }); + describe('clearExpiredProjects', function() { + beforeEach(function() { + this.project_ids = ['project-id-1', 'project-id-2'] + this.ProjectPersistenceManager._findExpiredProjectIds = sinon + .stub() + .callsArgWith(0, null, this.project_ids) + this.ProjectPersistenceManager.clearProjectFromCache = sinon + .stub() + .callsArg(1) + this.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1) + return this.ProjectPersistenceManager.clearExpiredProjects(this.callback) + }) - it("should clear each expired project", function() { - return Array.from(this.project_ids).map((project_id) => - this.ProjectPersistenceManager.clearProjectFromCache - .calledWith(project_id) - .should.equal(true)); - }); + it('should clear each expired project', function() { + return Array.from(this.project_ids).map(project_id => + this.ProjectPersistenceManager.clearProjectFromCache + .calledWith(project_id) + .should.equal(true) + ) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) - return describe("clearProject", function() { - beforeEach(function() { - this.ProjectPersistenceManager._clearProjectFromDatabase = sinon.stub().callsArg(1); - this.UrlCache.clearProject = sinon.stub().callsArg(1); - this.CompileManager.clearProject = sinon.stub().callsArg(2); - return this.ProjectPersistenceManager.clearProject(this.project_id, this.user_id, this.callback); - }); + return describe('clearProject', function() { + beforeEach(function() { + this.ProjectPersistenceManager._clearProjectFromDatabase = sinon + .stub() + .callsArg(1) + this.UrlCache.clearProject = sinon.stub().callsArg(1) + this.CompileManager.clearProject = sinon.stub().callsArg(2) + return this.ProjectPersistenceManager.clearProject( + this.project_id, + this.user_id, + this.callback + ) + }) - it("should clear the project from the database", function() { - return this.ProjectPersistenceManager._clearProjectFromDatabase - .calledWith(this.project_id) - .should.equal(true); - }); + it('should clear the project from the database', function() { + return this.ProjectPersistenceManager._clearProjectFromDatabase + .calledWith(this.project_id) + .should.equal(true) + }) - it("should clear all the cached Urls for the project", function() { - return this.UrlCache.clearProject - .calledWith(this.project_id) - .should.equal(true); - }); + it('should clear all the cached Urls for the project', function() { + return this.UrlCache.clearProject + .calledWith(this.project_id) + .should.equal(true) + }) - it("should clear the project compile folder", function() { - return this.CompileManager.clearProject - .calledWith(this.project_id, this.user_id) - .should.equal(true); - }); + it('should clear the project compile folder', function() { + return this.CompileManager.clearProject + .calledWith(this.project_id, this.user_id) + .should.equal(true) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); -}); - + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/test/unit/js/RequestParserTests.js b/test/unit/js/RequestParserTests.js index 725988f6..e2d8b026 100644 --- a/test/unit/js/RequestParserTests.js +++ b/test/unit/js/RequestParserTests.js @@ -9,378 +9,412 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const { expect } = require('chai'); -const modulePath = require('path').join(__dirname, '../../../app/js/RequestParser'); -const tk = require("timekeeper"); - -describe("RequestParser", function() { - beforeEach(function() { - tk.freeze(); - this.callback = sinon.stub(); - this.validResource = { - path: "main.tex", - date: "12:00 01/02/03", - content: "Hello world" - }; - this.validRequest = { - compile: { - token: "token-123", - options: { - imageName: "basicImageName/here:2017-1", - compiler: "pdflatex", - timeout: 42 - }, - resources: [] - } - }; - return this.RequestParser = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.settings = {}) - } - });}); - - afterEach(function() { return tk.reset(); }); - - describe("without a top level object", function() { - beforeEach(function() { - return this.RequestParser.parse([], this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("top level object should have a compile attribute") - .should.equal(true); - }); - }); - - describe("without a compile attribute", function() { - beforeEach(function() { - return this.RequestParser.parse({}, this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("top level object should have a compile attribute") - .should.equal(true); - }); - }); - - describe("without a valid compiler", function() { - beforeEach(function() { - this.validRequest.compile.options.compiler = "not-a-compiler"; - return this.RequestParser.parse(this.validRequest, this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("compiler attribute should be one of: pdflatex, latex, xelatex, lualatex") - .should.equal(true); - }); - }); - - describe("without a compiler specified", function() { - beforeEach(function() { - delete this.validRequest.compile.options.compiler; - return this.RequestParser.parse(this.validRequest, (error, data) => { - this.data = data; - - }); - }); - - return it("should set the compiler to pdflatex by default", function() { - return this.data.compiler.should.equal("pdflatex"); - }); - }); - - describe("with imageName set", function() { - beforeEach(function() { - return this.RequestParser.parse(this.validRequest, (error, data) => { - this.data = data; - - }); - }); - - return it("should set the imageName", function() { - return this.data.imageName.should.equal("basicImageName/here:2017-1"); - }); - }); - - describe("with flags set", function() { - beforeEach(function() { - this.validRequest.compile.options.flags = ["-file-line-error"]; - return this.RequestParser.parse(this.validRequest, (error, data) => { - this.data = data; - - }); - }); - - return it("should set the flags attribute", function() { - return expect(this.data.flags).to.deep.equal(["-file-line-error"]); - }); -}); - - describe("with flags not specified", function() { - beforeEach(function() { - return this.RequestParser.parse(this.validRequest, (error, data) => { - this.data = data; - - }); - }); - - return it("it should have an empty flags list", function() { - return expect(this.data.flags).to.deep.equal([]); - }); -}); - - describe("without a timeout specified", function() { - beforeEach(function() { - delete this.validRequest.compile.options.timeout; - return this.RequestParser.parse(this.validRequest, (error, data) => { - this.data = data; - - }); - }); - - return it("should set the timeout to MAX_TIMEOUT", function() { - return this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000); - }); - }); - - describe("with a timeout larger than the maximum", function() { - beforeEach(function() { - this.validRequest.compile.options.timeout = this.RequestParser.MAX_TIMEOUT + 1; - return this.RequestParser.parse(this.validRequest, (error, data) => { - this.data = data; - - }); - }); - - return it("should set the timeout to MAX_TIMEOUT", function() { - return this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000); - }); - }); - - describe("with a timeout", function() { - beforeEach(function() { - return this.RequestParser.parse(this.validRequest, (error, data) => { - this.data = data; - - }); - }); - - return it("should set the timeout (in milliseconds)", function() { - return this.data.timeout.should.equal(this.validRequest.compile.options.timeout * 1000); - }); - }); - - describe("with a resource without a path", function() { - beforeEach(function() { - delete this.validResource.path; - this.validRequest.compile.resources.push(this.validResource); - return this.RequestParser.parse(this.validRequest, this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("all resources should have a path attribute") - .should.equal(true); - }); - }); - - describe("with a resource with a path", function() { - beforeEach(function() { - this.validResource.path = (this.path = "test.tex"); - this.validRequest.compile.resources.push(this.validResource); - this.RequestParser.parse(this.validRequest, this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return the path in the parsed response", function() { - return this.data.resources[0].path.should.equal(this.path); - }); - }); - - describe("with a resource with a malformed modified date", function() { - beforeEach(function() { - this.validResource.modified = "not-a-date"; - this.validRequest.compile.resources.push(this.validResource); - return this.RequestParser.parse(this.validRequest, this.callback); - }); - - return it("should return an error", function() { - return this.callback - .calledWith( - "resource modified date could not be understood: "+ - this.validResource.modified - ) - .should.equal(true); - }); - }); - - describe("with a resource with a valid date", function() { - beforeEach(function() { - this.date = "12:00 01/02/03"; - this.validResource.modified = this.date; - this.validRequest.compile.resources.push(this.validResource); - this.RequestParser.parse(this.validRequest, this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return the date as a Javascript Date object", function() { - (this.data.resources[0].modified instanceof Date).should.equal(true); - return this.data.resources[0].modified.getTime().should.equal(Date.parse(this.date)); - }); - }); - - describe("with a resource without either a content or URL attribute", function() { - beforeEach(function() { - delete this.validResource.url; - delete this.validResource.content; - this.validRequest.compile.resources.push(this.validResource); - return this.RequestParser.parse(this.validRequest, this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("all resources should have either a url or content attribute") - .should.equal(true); - }); - }); - - describe("with a resource where the content is not a string", function() { - beforeEach(function() { - this.validResource.content = []; - this.validRequest.compile.resources.push(this.validResource); - return this.RequestParser.parse((this.validRequest), this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("content attribute should be a string") - .should.equal(true); - }); - }); - - describe("with a resource where the url is not a string", function() { - beforeEach(function() { - this.validResource.url = []; - this.validRequest.compile.resources.push(this.validResource); - return this.RequestParser.parse((this.validRequest), this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("url attribute should be a string") - .should.equal(true); - }); - }); - - describe("with a resource with a url", function() { - beforeEach(function() { - this.validResource.url = (this.url = "www.example.com"); - this.validRequest.compile.resources.push(this.validResource); - this.RequestParser.parse((this.validRequest), this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return the url in the parsed response", function() { - return this.data.resources[0].url.should.equal(this.url); - }); - }); - - describe("with a resource with a content attribute", function() { - beforeEach(function() { - this.validResource.content = (this.content = "Hello world"); - this.validRequest.compile.resources.push(this.validResource); - this.RequestParser.parse((this.validRequest), this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return the content in the parsed response", function() { - return this.data.resources[0].content.should.equal(this.content); - }); - }); - - describe("without a root resource path", function() { - beforeEach(function() { - delete this.validRequest.compile.rootResourcePath; - this.RequestParser.parse((this.validRequest), this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should set the root resource path to 'main.tex' by default", function() { - return this.data.rootResourcePath.should.equal("main.tex"); - }); - }); - - describe("with a root resource path", function() { - beforeEach(function() { - this.validRequest.compile.rootResourcePath = (this.path = "test.tex"); - this.RequestParser.parse((this.validRequest), this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return the root resource path in the parsed response", function() { - return this.data.rootResourcePath.should.equal(this.path); - }); - }); - - describe("with a root resource path that is not a string", function() { - beforeEach(function() { - this.validRequest.compile.rootResourcePath = []; - return this.RequestParser.parse((this.validRequest), this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith("rootResourcePath attribute should be a string") - .should.equal(true); - }); - }); - - describe("with a root resource path that needs escaping", function() { - beforeEach(function() { - this.badPath = "`rm -rf foo`.tex"; - this.goodPath = "rm -rf foo.tex"; - this.validRequest.compile.rootResourcePath = this.badPath; - this.validRequest.compile.resources.push({ - path: this.badPath, - date: "12:00 01/02/03", - content: "Hello world" - }); - this.RequestParser.parse(this.validRequest, this.callback); - return this.data = this.callback.args[0][1];}); - - it("should return the escaped resource", function() { - return this.data.rootResourcePath.should.equal(this.goodPath); - }); - - return it("should also escape the resource path", function() { - return this.data.resources[0].path.should.equal(this.goodPath); - }); - }); - - describe("with a root resource path that has a relative path", function() { - beforeEach(function() { - this.validRequest.compile.rootResourcePath = "foo/../../bar.tex"; - this.RequestParser.parse(this.validRequest, this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return an error", function() { - return this.callback.calledWith("relative path in root resource") - .should.equal(true); - }); - }); - - describe("with a root resource path that has unescaped + relative path", function() { - beforeEach(function() { - this.validRequest.compile.rootResourcePath = "foo/#../bar.tex"; - this.RequestParser.parse(this.validRequest, this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return an error", function() { - return this.callback.calledWith("relative path in root resource") - .should.equal(true); - }); - }); - - return describe("with an unknown syncType", function() { - beforeEach(function() { - this.validRequest.compile.options.syncType = "unexpected"; - this.RequestParser.parse(this.validRequest, this.callback); - return this.data = this.callback.args[0][1];}); - - return it("should return an error", function() { - return this.callback.calledWith("syncType attribute should be one of: full, incremental") - .should.equal(true); - }); - }); -}); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const { expect } = require('chai') +const modulePath = require('path').join( + __dirname, + '../../../app/js/RequestParser' +) +const tk = require('timekeeper') + +describe('RequestParser', function() { + beforeEach(function() { + tk.freeze() + this.callback = sinon.stub() + this.validResource = { + path: 'main.tex', + date: '12:00 01/02/03', + content: 'Hello world' + } + this.validRequest = { + compile: { + token: 'token-123', + options: { + imageName: 'basicImageName/here:2017-1', + compiler: 'pdflatex', + timeout: 42 + }, + resources: [] + } + } + return (this.RequestParser = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = {}) + } + })) + }) + + afterEach(function() { + return tk.reset() + }) + + describe('without a top level object', function() { + beforeEach(function() { + return this.RequestParser.parse([], this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('top level object should have a compile attribute') + .should.equal(true) + }) + }) + + describe('without a compile attribute', function() { + beforeEach(function() { + return this.RequestParser.parse({}, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('top level object should have a compile attribute') + .should.equal(true) + }) + }) + + describe('without a valid compiler', function() { + beforeEach(function() { + this.validRequest.compile.options.compiler = 'not-a-compiler' + return this.RequestParser.parse(this.validRequest, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith( + 'compiler attribute should be one of: pdflatex, latex, xelatex, lualatex' + ) + .should.equal(true) + }) + }) + + describe('without a compiler specified', function() { + beforeEach(function() { + delete this.validRequest.compile.options.compiler + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data + }) + }) + + return it('should set the compiler to pdflatex by default', function() { + return this.data.compiler.should.equal('pdflatex') + }) + }) + + describe('with imageName set', function() { + beforeEach(function() { + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data + }) + }) + + return it('should set the imageName', function() { + return this.data.imageName.should.equal('basicImageName/here:2017-1') + }) + }) + + describe('with flags set', function() { + beforeEach(function() { + this.validRequest.compile.options.flags = ['-file-line-error'] + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data + }) + }) + + return it('should set the flags attribute', function() { + return expect(this.data.flags).to.deep.equal(['-file-line-error']) + }) + }) + + describe('with flags not specified', function() { + beforeEach(function() { + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data + }) + }) + + return it('it should have an empty flags list', function() { + return expect(this.data.flags).to.deep.equal([]) + }) + }) + + describe('without a timeout specified', function() { + beforeEach(function() { + delete this.validRequest.compile.options.timeout + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data + }) + }) + + return it('should set the timeout to MAX_TIMEOUT', function() { + return this.data.timeout.should.equal( + this.RequestParser.MAX_TIMEOUT * 1000 + ) + }) + }) + + describe('with a timeout larger than the maximum', function() { + beforeEach(function() { + this.validRequest.compile.options.timeout = + this.RequestParser.MAX_TIMEOUT + 1 + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data + }) + }) + + return it('should set the timeout to MAX_TIMEOUT', function() { + return this.data.timeout.should.equal( + this.RequestParser.MAX_TIMEOUT * 1000 + ) + }) + }) + + describe('with a timeout', function() { + beforeEach(function() { + return this.RequestParser.parse(this.validRequest, (error, data) => { + this.data = data + }) + }) + + return it('should set the timeout (in milliseconds)', function() { + return this.data.timeout.should.equal( + this.validRequest.compile.options.timeout * 1000 + ) + }) + }) + + describe('with a resource without a path', function() { + beforeEach(function() { + delete this.validResource.path + this.validRequest.compile.resources.push(this.validResource) + return this.RequestParser.parse(this.validRequest, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('all resources should have a path attribute') + .should.equal(true) + }) + }) + + describe('with a resource with a path', function() { + beforeEach(function() { + this.validResource.path = this.path = 'test.tex' + this.validRequest.compile.resources.push(this.validResource) + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return the path in the parsed response', function() { + return this.data.resources[0].path.should.equal(this.path) + }) + }) + + describe('with a resource with a malformed modified date', function() { + beforeEach(function() { + this.validResource.modified = 'not-a-date' + this.validRequest.compile.resources.push(this.validResource) + return this.RequestParser.parse(this.validRequest, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith( + 'resource modified date could not be understood: ' + + this.validResource.modified + ) + .should.equal(true) + }) + }) + + describe('with a resource with a valid date', function() { + beforeEach(function() { + this.date = '12:00 01/02/03' + this.validResource.modified = this.date + this.validRequest.compile.resources.push(this.validResource) + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return the date as a Javascript Date object', function() { + ;(this.data.resources[0].modified instanceof Date).should.equal(true) + return this.data.resources[0].modified + .getTime() + .should.equal(Date.parse(this.date)) + }) + }) + + describe('with a resource without either a content or URL attribute', function() { + beforeEach(function() { + delete this.validResource.url + delete this.validResource.content + this.validRequest.compile.resources.push(this.validResource) + return this.RequestParser.parse(this.validRequest, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith( + 'all resources should have either a url or content attribute' + ) + .should.equal(true) + }) + }) + + describe('with a resource where the content is not a string', function() { + beforeEach(function() { + this.validResource.content = [] + this.validRequest.compile.resources.push(this.validResource) + return this.RequestParser.parse(this.validRequest, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('content attribute should be a string') + .should.equal(true) + }) + }) + + describe('with a resource where the url is not a string', function() { + beforeEach(function() { + this.validResource.url = [] + this.validRequest.compile.resources.push(this.validResource) + return this.RequestParser.parse(this.validRequest, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('url attribute should be a string') + .should.equal(true) + }) + }) + + describe('with a resource with a url', function() { + beforeEach(function() { + this.validResource.url = this.url = 'www.example.com' + this.validRequest.compile.resources.push(this.validResource) + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return the url in the parsed response', function() { + return this.data.resources[0].url.should.equal(this.url) + }) + }) + + describe('with a resource with a content attribute', function() { + beforeEach(function() { + this.validResource.content = this.content = 'Hello world' + this.validRequest.compile.resources.push(this.validResource) + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return the content in the parsed response', function() { + return this.data.resources[0].content.should.equal(this.content) + }) + }) + + describe('without a root resource path', function() { + beforeEach(function() { + delete this.validRequest.compile.rootResourcePath + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it("should set the root resource path to 'main.tex' by default", function() { + return this.data.rootResourcePath.should.equal('main.tex') + }) + }) + + describe('with a root resource path', function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = this.path = 'test.tex' + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return the root resource path in the parsed response', function() { + return this.data.rootResourcePath.should.equal(this.path) + }) + }) + + describe('with a root resource path that is not a string', function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = [] + return this.RequestParser.parse(this.validRequest, this.callback) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('rootResourcePath attribute should be a string') + .should.equal(true) + }) + }) + + describe('with a root resource path that needs escaping', function() { + beforeEach(function() { + this.badPath = '`rm -rf foo`.tex' + this.goodPath = 'rm -rf foo.tex' + this.validRequest.compile.rootResourcePath = this.badPath + this.validRequest.compile.resources.push({ + path: this.badPath, + date: '12:00 01/02/03', + content: 'Hello world' + }) + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + it('should return the escaped resource', function() { + return this.data.rootResourcePath.should.equal(this.goodPath) + }) + + return it('should also escape the resource path', function() { + return this.data.resources[0].path.should.equal(this.goodPath) + }) + }) + + describe('with a root resource path that has a relative path', function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = 'foo/../../bar.tex' + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('relative path in root resource') + .should.equal(true) + }) + }) + + describe('with a root resource path that has unescaped + relative path', function() { + beforeEach(function() { + this.validRequest.compile.rootResourcePath = 'foo/#../bar.tex' + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('relative path in root resource') + .should.equal(true) + }) + }) + + return describe('with an unknown syncType', function() { + beforeEach(function() { + this.validRequest.compile.options.syncType = 'unexpected' + this.RequestParser.parse(this.validRequest, this.callback) + return (this.data = this.callback.args[0][1]) + }) + + return it('should return an error', function() { + return this.callback + .calledWith('syncType attribute should be one of: full, incremental') + .should.equal(true) + }) + }) +}) diff --git a/test/unit/js/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js index fe52cc58..c0e89ef7 100644 --- a/test/unit/js/ResourceStateManagerTests.js +++ b/test/unit/js/ResourceStateManagerTests.js @@ -9,145 +9,200 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -const should = require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/ResourceStateManager'); -const Path = require("path"); -const Errors = require("../../../app/js/Errors"); - -describe("ResourceStateManager", function() { - beforeEach(function() { - this.ResourceStateManager = SandboxedModule.require(modulePath, { requires: { - "fs": (this.fs = {}), - "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()}, - "./SafeReader": (this.SafeReader = {}) - } - }); - this.basePath = "/path/to/write/files/to"; - this.resources = [ - {path: "resource-1-mock"}, - {path: "resource-2-mock"}, - {path: "resource-3-mock"} - ]; - this.state = "1234567890"; - this.resourceFileName = `${this.basePath}/.project-sync-state`; - this.resourceFileContents = `${this.resources[0].path}\n${this.resources[1].path}\n${this.resources[2].path}\nstateHash:${this.state}`; - return this.callback = sinon.stub(); - }); - - describe("saveProjectState", function() { - beforeEach(function() { - return this.fs.writeFile = sinon.stub().callsArg(2); - }); - - describe("when the state is specified", function() { - beforeEach(function() { - return this.ResourceStateManager.saveProjectState(this.state, this.resources, this.basePath, this.callback); - }); - - it("should write the resource list to disk", function() { - return this.fs.writeFile - .calledWith(this.resourceFileName, this.resourceFileContents) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("when the state is undefined", function() { - beforeEach(function() { - this.state = undefined; - this.fs.unlink = sinon.stub().callsArg(1); - return this.ResourceStateManager.saveProjectState(this.state, this.resources, this.basePath, this.callback); - }); - - it("should unlink the resource file", function() { - return this.fs.unlink - .calledWith(this.resourceFileName) - .should.equal(true); - }); - - it("should not write the resource list to disk", function() { - return this.fs.writeFile.called.should.equal(false); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - describe("checkProjectStateMatches", function() { - - describe("when the state matches", function() { - beforeEach(function() { - this.SafeReader.readFile = sinon.stub().callsArgWith(3, null, this.resourceFileContents); - return this.ResourceStateManager.checkProjectStateMatches(this.state, this.basePath, this.callback); - }); - - it("should read the resource file", function() { - return this.SafeReader.readFile - .calledWith(this.resourceFileName) - .should.equal(true); - }); - - return it("should call the callback with the results", function() { - return this.callback.calledWithMatch(null, this.resources).should.equal(true); - }); - }); - - return describe("when the state does not match", function() { - beforeEach(function() { - this.SafeReader.readFile = sinon.stub().callsArgWith(3, null, this.resourceFileContents); - return this.ResourceStateManager.checkProjectStateMatches("not-the-original-state", this.basePath, this.callback); - }); - - return it("should call the callback with an error", function() { - const error = new Errors.FilesOutOfSyncError("invalid state for incremental update"); - return this.callback.calledWith(error).should.equal(true); - }); - }); - }); - - return describe("checkResourceFiles", function() { - describe("when all the files are present", function() { - beforeEach(function() { - this.allFiles = [ this.resources[0].path, this.resources[1].path, this.resources[2].path]; - return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback); - }); - - return it("should call the callback", function() { - return this.callback.calledWithExactly().should.equal(true); - }); - }); - - describe("when there is a missing file", function() { - beforeEach(function() { - this.allFiles = [ this.resources[0].path, this.resources[1].path]; - this.fs.stat = sinon.stub().callsArgWith(1, new Error()); - return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback); - }); - - return it("should call the callback with an error", function() { - const error = new Errors.FilesOutOfSyncError("resource files missing in incremental update"); - return this.callback.calledWith(error).should.equal(true); - }); - }); - - return describe("when a resource contains a relative path", function() { - beforeEach(function() { - this.resources[0].path = "../foo/bar.tex"; - this.allFiles = [ this.resources[0].path, this.resources[1].path, this.resources[2].path]; - return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback); - }); - - return it("should call the callback with an error", function() { - return this.callback.calledWith(new Error("relative path in resource file list")).should.equal(true); - }); - }); - }); -}); - +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const should = require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/ResourceStateManager' +) +const Path = require('path') +const Errors = require('../../../app/js/Errors') + +describe('ResourceStateManager', function() { + beforeEach(function() { + this.ResourceStateManager = SandboxedModule.require(modulePath, { + requires: { + fs: (this.fs = {}), + 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, + './SafeReader': (this.SafeReader = {}) + } + }) + this.basePath = '/path/to/write/files/to' + this.resources = [ + { path: 'resource-1-mock' }, + { path: 'resource-2-mock' }, + { path: 'resource-3-mock' } + ] + this.state = '1234567890' + this.resourceFileName = `${this.basePath}/.project-sync-state` + this.resourceFileContents = `${this.resources[0].path}\n${this.resources[1].path}\n${this.resources[2].path}\nstateHash:${this.state}` + return (this.callback = sinon.stub()) + }) + + describe('saveProjectState', function() { + beforeEach(function() { + return (this.fs.writeFile = sinon.stub().callsArg(2)) + }) + + describe('when the state is specified', function() { + beforeEach(function() { + return this.ResourceStateManager.saveProjectState( + this.state, + this.resources, + this.basePath, + this.callback + ) + }) + + it('should write the resource list to disk', function() { + return this.fs.writeFile + .calledWith(this.resourceFileName, this.resourceFileContents) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the state is undefined', function() { + beforeEach(function() { + this.state = undefined + this.fs.unlink = sinon.stub().callsArg(1) + return this.ResourceStateManager.saveProjectState( + this.state, + this.resources, + this.basePath, + this.callback + ) + }) + + it('should unlink the resource file', function() { + return this.fs.unlink + .calledWith(this.resourceFileName) + .should.equal(true) + }) + + it('should not write the resource list to disk', function() { + return this.fs.writeFile.called.should.equal(false) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('checkProjectStateMatches', function() { + describe('when the state matches', function() { + beforeEach(function() { + this.SafeReader.readFile = sinon + .stub() + .callsArgWith(3, null, this.resourceFileContents) + return this.ResourceStateManager.checkProjectStateMatches( + this.state, + this.basePath, + this.callback + ) + }) + + it('should read the resource file', function() { + return this.SafeReader.readFile + .calledWith(this.resourceFileName) + .should.equal(true) + }) + + return it('should call the callback with the results', function() { + return this.callback + .calledWithMatch(null, this.resources) + .should.equal(true) + }) + }) + + return describe('when the state does not match', function() { + beforeEach(function() { + this.SafeReader.readFile = sinon + .stub() + .callsArgWith(3, null, this.resourceFileContents) + return this.ResourceStateManager.checkProjectStateMatches( + 'not-the-original-state', + this.basePath, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + const error = new Errors.FilesOutOfSyncError( + 'invalid state for incremental update' + ) + return this.callback.calledWith(error).should.equal(true) + }) + }) + }) + + return describe('checkResourceFiles', function() { + describe('when all the files are present', function() { + beforeEach(function() { + this.allFiles = [ + this.resources[0].path, + this.resources[1].path, + this.resources[2].path + ] + return this.ResourceStateManager.checkResourceFiles( + this.resources, + this.allFiles, + this.basePath, + this.callback + ) + }) + + return it('should call the callback', function() { + return this.callback.calledWithExactly().should.equal(true) + }) + }) + + describe('when there is a missing file', function() { + beforeEach(function() { + this.allFiles = [this.resources[0].path, this.resources[1].path] + this.fs.stat = sinon.stub().callsArgWith(1, new Error()) + return this.ResourceStateManager.checkResourceFiles( + this.resources, + this.allFiles, + this.basePath, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + const error = new Errors.FilesOutOfSyncError( + 'resource files missing in incremental update' + ) + return this.callback.calledWith(error).should.equal(true) + }) + }) + + return describe('when a resource contains a relative path', function() { + beforeEach(function() { + this.resources[0].path = '../foo/bar.tex' + this.allFiles = [ + this.resources[0].path, + this.resources[1].path, + this.resources[2].path + ] + return this.ResourceStateManager.checkResourceFiles( + this.resources, + this.allFiles, + this.basePath, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWith(new Error('relative path in resource file list')) + .should.equal(true) + }) + }) + }) +}) diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 83095473..189908dc 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -10,405 +10,491 @@ * DS206: Consider reworking classes to avoid initClass * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -const should = require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/ResourceWriter'); -const path = require("path"); - -describe("ResourceWriter", function() { - beforeEach(function() { - let Timer; - this.ResourceWriter = SandboxedModule.require(modulePath, { requires: { - "fs": (this.fs = { - mkdir: sinon.stub().callsArg(1), - unlink: sinon.stub().callsArg(1) - }), - "./ResourceStateManager": (this.ResourceStateManager = {}), - "wrench": (this.wrench = {}), - "./UrlCache" : (this.UrlCache = {}), - "mkdirp" : (this.mkdirp = sinon.stub().callsArg(1)), - "./OutputFileFinder": (this.OutputFileFinder = {}), - "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()}, - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()) - }) - } - } - ); - this.project_id = "project-id-123"; - this.basePath = "/path/to/write/files/to"; - return this.callback = sinon.stub(); - }); - - describe("syncResourcesToDisk on a full request", function() { - beforeEach(function() { - this.resources = [ - "resource-1-mock", - "resource-2-mock", - "resource-3-mock" - ]; - this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3); - this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2); - this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3); - return this.ResourceWriter.syncResourcesToDisk({ - project_id: this.project_id, - syncState: (this.syncState = "0123456789abcdef"), - resources: this.resources - }, this.basePath, this.callback); - }); - - it("should remove old files", function() { - return this.ResourceWriter._removeExtraneousFiles - .calledWith(this.resources, this.basePath) - .should.equal(true); - }); - - it("should write each resource to disk", function() { - return Array.from(this.resources).map((resource) => - this.ResourceWriter._writeResourceToDisk - .calledWith(this.project_id, resource, this.basePath) - .should.equal(true)); - }); - - it("should store the sync state and resource list", function() { - return this.ResourceStateManager.saveProjectState - .calledWith(this.syncState, this.resources, this.basePath) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("syncResourcesToDisk on an incremental update", function() { - beforeEach(function() { - this.resources = [ - "resource-1-mock" - ]; - this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3); - this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArgWith(2, null, (this.outputFiles = []), (this.allFiles = [])); - this.ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, null, this.resources); - this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3); - this.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3); - return this.ResourceWriter.syncResourcesToDisk({ - project_id: this.project_id, - syncType: "incremental", - syncState: (this.syncState = "1234567890abcdef"), - resources: this.resources - }, this.basePath, this.callback); - }); - - it("should check the sync state matches", function() { - return this.ResourceStateManager.checkProjectStateMatches - .calledWith(this.syncState, this.basePath) - .should.equal(true); - }); - - it("should remove old files", function() { - return this.ResourceWriter._removeExtraneousFiles - .calledWith(this.resources, this.basePath) - .should.equal(true); - }); - - it("should check each resource exists", function() { - return this.ResourceStateManager.checkResourceFiles - .calledWith(this.resources, this.allFiles, this.basePath) - .should.equal(true); - }); - - it("should write each resource to disk", function() { - return Array.from(this.resources).map((resource) => - this.ResourceWriter._writeResourceToDisk - .calledWith(this.project_id, resource, this.basePath) - .should.equal(true)); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("syncResourcesToDisk on an incremental update when the state does not match", function() { - beforeEach(function() { - this.resources = [ - "resource-1-mock" - ]; - this.ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, (this.error = new Error())); - return this.ResourceWriter.syncResourcesToDisk({ - project_id: this.project_id, - syncType: "incremental", - syncState: (this.syncState = "1234567890abcdef"), - resources: this.resources - }, this.basePath, this.callback); - }); - - it("should check whether the sync state matches", function() { - return this.ResourceStateManager.checkProjectStateMatches - .calledWith(this.syncState, this.basePath) - .should.equal(true); - }); - - return it("should call the callback with an error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - - - describe("_removeExtraneousFiles", function() { - beforeEach(function() { - this.output_files = [{ - path: "output.pdf", - type: "pdf" - }, { - path: "extra/file.tex", - type: "tex" - }, { - path: "extra.aux", - type: "aux" - }, { - path: "cache/_chunk1" - },{ - path: "figures/image-eps-converted-to.pdf", - type: "pdf" - },{ - path: "foo/main-figure0.md5", - type: "md5" - }, { - path: "foo/main-figure0.dpth", - type: "dpth" - }, { - path: "foo/main-figure0.pdf", - type: "pdf" - }, { - path: "_minted-main/default-pyg-prefix.pygstyle", - type: "pygstyle" - }, { - path: "_minted-main/default.pygstyle", - type: "pygstyle" - }, { - path: "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex", - type: "pygtex" - }, { - path: "_markdown_main/30893013dec5d869a415610079774c2f.md.tex", - type: "tex" - }]; - this.resources = "mock-resources"; - this.OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, this.output_files); - this.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1); - return this.ResourceWriter._removeExtraneousFiles(this.resources, this.basePath, this.callback); - }); - - it("should find the existing output files", function() { - return this.OutputFileFinder.findOutputFiles - .calledWith(this.resources, this.basePath) - .should.equal(true); - }); - - it("should delete the output files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "output.pdf")) - .should.equal(true); - }); - - it("should delete the extra files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "extra/file.tex")) - .should.equal(true); - }); - - it("should not delete the extra aux files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "extra.aux")) - .should.equal(false); - }); - - it("should not delete the knitr cache file", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "cache/_chunk1")) - .should.equal(false); - }); - - it("should not delete the epstopdf converted files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "figures/image-eps-converted-to.pdf")) - .should.equal(false); - }); - - it("should not delete the tikz md5 files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "foo/main-figure0.md5")) - .should.equal(false); - }); - - it("should not delete the tikz dpth files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "foo/main-figure0.dpth")) - .should.equal(false); - }); - - it("should not delete the tikz pdf files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "foo/main-figure0.pdf")) - .should.equal(false); - }); - - it("should not delete the minted pygstyle files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "_minted-main/default-pyg-prefix.pygstyle")) - .should.equal(false); - }); - - it("should not delete the minted default pygstyle files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "_minted-main/default.pygstyle")) - .should.equal(false); - }); - - it("should not delete the minted default pygtex files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex")) - .should.equal(false); - }); - - it("should not delete the markdown md.tex files", function() { - return this.ResourceWriter._deleteFileIfNotDirectory - .calledWith(path.join(this.basePath, "_markdown_main/30893013dec5d869a415610079774c2f.md.tex")) - .should.equal(false); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("_writeResourceToDisk", function() { - describe("with a url based resource", function() { - beforeEach(function() { - this.resource = { - path: "main.tex", - url: "http://www.example.com/main.tex", - modified: Date.now() - }; - this.UrlCache.downloadUrlToFile = sinon.stub().callsArgWith(4, "fake error downloading file"); - return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback); - }); - - it("should ensure the directory exists", function() { - return this.mkdirp - .calledWith(path.dirname(path.join(this.basePath, this.resource.path))) - .should.equal(true); - }); - - it("should write the URL from the cache", function() { - return this.UrlCache.downloadUrlToFile - .calledWith(this.project_id, this.resource.url, path.join(this.basePath, this.resource.path), this.resource.modified) - .should.equal(true); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should not return an error if the resource writer errored", function() { - return should.not.exist(this.callback.args[0][0]); - }); - }); - - describe("with a content based resource", function() { - beforeEach(function() { - this.resource = { - path: "main.tex", - content: "Hello world" - }; - this.fs.writeFile = sinon.stub().callsArg(2); - return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback); - }); - - it("should ensure the directory exists", function() { - return this.mkdirp - .calledWith(path.dirname(path.join(this.basePath, this.resource.path))) - .should.equal(true); - }); - - it("should write the contents to disk", function() { - return this.fs.writeFile - .calledWith(path.join(this.basePath, this.resource.path), this.resource.content) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("with a file path that breaks out of the root folder", function() { - beforeEach(function() { - this.resource = { - path: "../../main.tex", - content: "Hello world" - }; - this.fs.writeFile = sinon.stub().callsArg(2); - return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback); - }); - - it("should not write to disk", function() { - return this.fs.writeFile.called.should.equal(false); - }); - - return it("should return an error", function() { - return this.callback - .calledWith(new Error("resource path is outside root directory")) - .should.equal(true); - }); - }); - }); - - return describe("checkPath", function() { - describe("with a valid path", function() { - beforeEach(function() { - return this.ResourceWriter.checkPath("foo", "bar", this.callback); - }); - - return it("should return the joined path", function() { - return this.callback.calledWith(null, "foo/bar") - .should.equal(true); - }); - }); - - describe("with an invalid path", function() { - beforeEach(function() { - return this.ResourceWriter.checkPath("foo", "baz/../../bar", this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith(new Error("resource path is outside root directory")) - .should.equal(true); - }); - }); - - return describe("with another invalid path matching on a prefix", function() { - beforeEach(function() { - return this.ResourceWriter.checkPath("foo", "../foobar/baz", this.callback); - }); - - return it("should return an error", function() { - return this.callback.calledWith(new Error("resource path is outside root directory")) - .should.equal(true); - }); - }); - }); -}); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const should = require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/ResourceWriter' +) +const path = require('path') + +describe('ResourceWriter', function() { + beforeEach(function() { + let Timer + this.ResourceWriter = SandboxedModule.require(modulePath, { + requires: { + fs: (this.fs = { + mkdir: sinon.stub().callsArg(1), + unlink: sinon.stub().callsArg(1) + }), + './ResourceStateManager': (this.ResourceStateManager = {}), + wrench: (this.wrench = {}), + './UrlCache': (this.UrlCache = {}), + mkdirp: (this.mkdirp = sinon.stub().callsArg(1)), + './OutputFileFinder': (this.OutputFileFinder = {}), + 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, + './Metrics': (this.Metrics = { + Timer: (Timer = (function() { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + }) + } + }) + this.project_id = 'project-id-123' + this.basePath = '/path/to/write/files/to' + return (this.callback = sinon.stub()) + }) + + describe('syncResourcesToDisk on a full request', function() { + beforeEach(function() { + this.resources = ['resource-1-mock', 'resource-2-mock', 'resource-3-mock'] + this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) + this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) + this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3) + return this.ResourceWriter.syncResourcesToDisk( + { + project_id: this.project_id, + syncState: (this.syncState = '0123456789abcdef'), + resources: this.resources + }, + this.basePath, + this.callback + ) + }) + + it('should remove old files', function() { + return this.ResourceWriter._removeExtraneousFiles + .calledWith(this.resources, this.basePath) + .should.equal(true) + }) + + it('should write each resource to disk', function() { + return Array.from(this.resources).map(resource => + this.ResourceWriter._writeResourceToDisk + .calledWith(this.project_id, resource, this.basePath) + .should.equal(true) + ) + }) + + it('should store the sync state and resource list', function() { + return this.ResourceStateManager.saveProjectState + .calledWith(this.syncState, this.resources, this.basePath) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('syncResourcesToDisk on an incremental update', function() { + beforeEach(function() { + this.resources = ['resource-1-mock'] + this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) + this.ResourceWriter._removeExtraneousFiles = sinon + .stub() + .callsArgWith(2, null, (this.outputFiles = []), (this.allFiles = [])) + this.ResourceStateManager.checkProjectStateMatches = sinon + .stub() + .callsArgWith(2, null, this.resources) + this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3) + this.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3) + return this.ResourceWriter.syncResourcesToDisk( + { + project_id: this.project_id, + syncType: 'incremental', + syncState: (this.syncState = '1234567890abcdef'), + resources: this.resources + }, + this.basePath, + this.callback + ) + }) + + it('should check the sync state matches', function() { + return this.ResourceStateManager.checkProjectStateMatches + .calledWith(this.syncState, this.basePath) + .should.equal(true) + }) + + it('should remove old files', function() { + return this.ResourceWriter._removeExtraneousFiles + .calledWith(this.resources, this.basePath) + .should.equal(true) + }) + + it('should check each resource exists', function() { + return this.ResourceStateManager.checkResourceFiles + .calledWith(this.resources, this.allFiles, this.basePath) + .should.equal(true) + }) + + it('should write each resource to disk', function() { + return Array.from(this.resources).map(resource => + this.ResourceWriter._writeResourceToDisk + .calledWith(this.project_id, resource, this.basePath) + .should.equal(true) + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('syncResourcesToDisk on an incremental update when the state does not match', function() { + beforeEach(function() { + this.resources = ['resource-1-mock'] + this.ResourceStateManager.checkProjectStateMatches = sinon + .stub() + .callsArgWith(2, (this.error = new Error())) + return this.ResourceWriter.syncResourcesToDisk( + { + project_id: this.project_id, + syncType: 'incremental', + syncState: (this.syncState = '1234567890abcdef'), + resources: this.resources + }, + this.basePath, + this.callback + ) + }) + + it('should check whether the sync state matches', function() { + return this.ResourceStateManager.checkProjectStateMatches + .calledWith(this.syncState, this.basePath) + .should.equal(true) + }) + + return it('should call the callback with an error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + describe('_removeExtraneousFiles', function() { + beforeEach(function() { + this.output_files = [ + { + path: 'output.pdf', + type: 'pdf' + }, + { + path: 'extra/file.tex', + type: 'tex' + }, + { + path: 'extra.aux', + type: 'aux' + }, + { + path: 'cache/_chunk1' + }, + { + path: 'figures/image-eps-converted-to.pdf', + type: 'pdf' + }, + { + path: 'foo/main-figure0.md5', + type: 'md5' + }, + { + path: 'foo/main-figure0.dpth', + type: 'dpth' + }, + { + path: 'foo/main-figure0.pdf', + type: 'pdf' + }, + { + path: '_minted-main/default-pyg-prefix.pygstyle', + type: 'pygstyle' + }, + { + path: '_minted-main/default.pygstyle', + type: 'pygstyle' + }, + { + path: + '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex', + type: 'pygtex' + }, + { + path: '_markdown_main/30893013dec5d869a415610079774c2f.md.tex', + type: 'tex' + } + ] + this.resources = 'mock-resources' + this.OutputFileFinder.findOutputFiles = sinon + .stub() + .callsArgWith(2, null, this.output_files) + this.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1) + return this.ResourceWriter._removeExtraneousFiles( + this.resources, + this.basePath, + this.callback + ) + }) + + it('should find the existing output files', function() { + return this.OutputFileFinder.findOutputFiles + .calledWith(this.resources, this.basePath) + .should.equal(true) + }) + + it('should delete the output files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'output.pdf')) + .should.equal(true) + }) + + it('should delete the extra files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'extra/file.tex')) + .should.equal(true) + }) + + it('should not delete the extra aux files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'extra.aux')) + .should.equal(false) + }) + + it('should not delete the knitr cache file', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'cache/_chunk1')) + .should.equal(false) + }) + + it('should not delete the epstopdf converted files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith( + path.join(this.basePath, 'figures/image-eps-converted-to.pdf') + ) + .should.equal(false) + }) + + it('should not delete the tikz md5 files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'foo/main-figure0.md5')) + .should.equal(false) + }) + + it('should not delete the tikz dpth files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'foo/main-figure0.dpth')) + .should.equal(false) + }) + + it('should not delete the tikz pdf files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'foo/main-figure0.pdf')) + .should.equal(false) + }) + + it('should not delete the minted pygstyle files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith( + path.join(this.basePath, '_minted-main/default-pyg-prefix.pygstyle') + ) + .should.equal(false) + }) + + it('should not delete the minted default pygstyle files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, '_minted-main/default.pygstyle')) + .should.equal(false) + }) + + it('should not delete the minted default pygtex files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith( + path.join( + this.basePath, + '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex' + ) + ) + .should.equal(false) + }) + + it('should not delete the markdown md.tex files', function() { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith( + path.join( + this.basePath, + '_markdown_main/30893013dec5d869a415610079774c2f.md.tex' + ) + ) + .should.equal(false) + }) + + it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + + return it('should time the request', function() { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('_writeResourceToDisk', function() { + describe('with a url based resource', function() { + beforeEach(function() { + this.resource = { + path: 'main.tex', + url: 'http://www.example.com/main.tex', + modified: Date.now() + } + this.UrlCache.downloadUrlToFile = sinon + .stub() + .callsArgWith(4, 'fake error downloading file') + return this.ResourceWriter._writeResourceToDisk( + this.project_id, + this.resource, + this.basePath, + this.callback + ) + }) + + it('should ensure the directory exists', function() { + return this.mkdirp + .calledWith( + path.dirname(path.join(this.basePath, this.resource.path)) + ) + .should.equal(true) + }) + + it('should write the URL from the cache', function() { + return this.UrlCache.downloadUrlToFile + .calledWith( + this.project_id, + this.resource.url, + path.join(this.basePath, this.resource.path), + this.resource.modified + ) + .should.equal(true) + }) + + it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + + return it('should not return an error if the resource writer errored', function() { + return should.not.exist(this.callback.args[0][0]) + }) + }) + + describe('with a content based resource', function() { + beforeEach(function() { + this.resource = { + path: 'main.tex', + content: 'Hello world' + } + this.fs.writeFile = sinon.stub().callsArg(2) + return this.ResourceWriter._writeResourceToDisk( + this.project_id, + this.resource, + this.basePath, + this.callback + ) + }) + + it('should ensure the directory exists', function() { + return this.mkdirp + .calledWith( + path.dirname(path.join(this.basePath, this.resource.path)) + ) + .should.equal(true) + }) + + it('should write the contents to disk', function() { + return this.fs.writeFile + .calledWith( + path.join(this.basePath, this.resource.path), + this.resource.content + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('with a file path that breaks out of the root folder', function() { + beforeEach(function() { + this.resource = { + path: '../../main.tex', + content: 'Hello world' + } + this.fs.writeFile = sinon.stub().callsArg(2) + return this.ResourceWriter._writeResourceToDisk( + this.project_id, + this.resource, + this.basePath, + this.callback + ) + }) + + it('should not write to disk', function() { + return this.fs.writeFile.called.should.equal(false) + }) + + return it('should return an error', function() { + return this.callback + .calledWith(new Error('resource path is outside root directory')) + .should.equal(true) + }) + }) + }) + + return describe('checkPath', function() { + describe('with a valid path', function() { + beforeEach(function() { + return this.ResourceWriter.checkPath('foo', 'bar', this.callback) + }) + + return it('should return the joined path', function() { + return this.callback.calledWith(null, 'foo/bar').should.equal(true) + }) + }) + + describe('with an invalid path', function() { + beforeEach(function() { + return this.ResourceWriter.checkPath( + 'foo', + 'baz/../../bar', + this.callback + ) + }) + + return it('should return an error', function() { + return this.callback + .calledWith(new Error('resource path is outside root directory')) + .should.equal(true) + }) + }) + + return describe('with another invalid path matching on a prefix', function() { + beforeEach(function() { + return this.ResourceWriter.checkPath( + 'foo', + '../foobar/baz', + this.callback + ) + }) + + return it('should return an error', function() { + return this.callback + .calledWith(new Error('resource path is outside root directory')) + .should.equal(true) + }) + }) + }) +}) diff --git a/test/unit/js/StaticServerForbidSymlinksTests.js b/test/unit/js/StaticServerForbidSymlinksTests.js index e754ea75..b9545a4c 100644 --- a/test/unit/js/StaticServerForbidSymlinksTests.js +++ b/test/unit/js/StaticServerForbidSymlinksTests.js @@ -9,217 +9,229 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const should = require('chai').should(); -const SandboxedModule = require('sandboxed-module'); -const assert = require('assert'); -const path = require('path'); -const sinon = require('sinon'); -const modulePath = path.join(__dirname, "../../../app/js/StaticServerForbidSymlinks"); -const { expect } = require("chai"); - -describe("StaticServerForbidSymlinks", function() { - - beforeEach(function() { - - this.settings = { - path: { - compilesDir: "/compiles/here" - } - }; - - this.fs = {}; - this.ForbidSymlinks = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex":this.settings, - "logger-sharelatex": { - log() {}, - warn() {}, - error() {} - }, - "fs":this.fs - } - } - ); - - this.dummyStatic = (rootDir, options) => - (req, res, next) => - // console.log "dummyStatic serving file", rootDir, "called with", req.url - // serve it - next() - - ; - - this.StaticServerForbidSymlinks = this.ForbidSymlinks(this.dummyStatic, this.settings.path.compilesDir); - this.req = { - params: { - project_id:"12345" - } - }; - - this.res = {}; - return this.req.url = "/12345/output.pdf"; - }); - - - describe("sending a normal file through", function() { - beforeEach(function() { - return this.fs.realpath = sinon.stub().callsArgWith(1, null, `${this.settings.path.compilesDir}/${this.req.params.project_id}/output.pdf`); - }); - - return it("should call next", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(200); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res, done); - }); - }); - - - describe("with a missing file", function() { - beforeEach(function() { - return this.fs.realpath = sinon.stub().callsArgWith(1, {code: 'ENOENT'}, `${this.settings.path.compilesDir}/${this.req.params.project_id}/unknown.pdf`); - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - - describe("with a symlink file", function() { - beforeEach(function() { - return this.fs.realpath = sinon.stub().callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`); - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - - describe("with a relative file", function() { - beforeEach(function() { - return this.req.url = "/12345/../67890/output.pdf"; - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - - describe("with a unnormalized file containing .", function() { - beforeEach(function() { - return this.req.url = "/12345/foo/./output.pdf"; - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - - describe("with a file containing an empty path", function() { - beforeEach(function() { - return this.req.url = "/12345/foo//output.pdf"; - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - describe("with a non-project file", function() { - beforeEach(function() { - return this.req.url = "/.foo/output.pdf"; - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - describe("with a file outside the compiledir", function() { - beforeEach(function() { - return this.req.url = "/../bar/output.pdf"; - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - - describe("with a file with no leading /", function() { - beforeEach(function() { - return this.req.url = "./../bar/output.pdf"; - }); - - return it("should send a 404", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(404); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); - - describe("with a github style path", function() { - beforeEach(function() { - this.req.url = "/henryoswald-latex_example/output/output.log"; - return this.fs.realpath = sinon.stub().callsArgWith(1, null, `${this.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`); - }); - - return it("should call next", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(200); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res, done); - }); - }); - - return describe("with an error from fs.realpath", function() { - - beforeEach(function() { - return this.fs.realpath = sinon.stub().callsArgWith(1, "error"); - }); - - return it("should send a 500", function(done){ - this.res.sendStatus = function(resCode){ - resCode.should.equal(500); - return done(); - }; - return this.StaticServerForbidSymlinks(this.req, this.res); - }); - }); -}); - +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../app/js/StaticServerForbidSymlinks' +) +const { expect } = require('chai') + +describe('StaticServerForbidSymlinks', function() { + beforeEach(function() { + this.settings = { + path: { + compilesDir: '/compiles/here' + } + } + + this.fs = {} + this.ForbidSymlinks = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {}, + warn() {}, + error() {} + }, + fs: this.fs + } + }) + + this.dummyStatic = (rootDir, options) => (req, res, next) => + // console.log "dummyStatic serving file", rootDir, "called with", req.url + // serve it + next() + + this.StaticServerForbidSymlinks = this.ForbidSymlinks( + this.dummyStatic, + this.settings.path.compilesDir + ) + this.req = { + params: { + project_id: '12345' + } + } + + this.res = {} + return (this.req.url = '/12345/output.pdf') + }) + + describe('sending a normal file through', function() { + beforeEach(function() { + return (this.fs.realpath = sinon + .stub() + .callsArgWith( + 1, + null, + `${this.settings.path.compilesDir}/${this.req.params.project_id}/output.pdf` + )) + }) + + return it('should call next', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(200) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res, done) + }) + }) + + describe('with a missing file', function() { + beforeEach(function() { + return (this.fs.realpath = sinon + .stub() + .callsArgWith( + 1, + { code: 'ENOENT' }, + `${this.settings.path.compilesDir}/${this.req.params.project_id}/unknown.pdf` + )) + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a symlink file', function() { + beforeEach(function() { + return (this.fs.realpath = sinon + .stub() + .callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`)) + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a relative file', function() { + beforeEach(function() { + return (this.req.url = '/12345/../67890/output.pdf') + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a unnormalized file containing .', function() { + beforeEach(function() { + return (this.req.url = '/12345/foo/./output.pdf') + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a file containing an empty path', function() { + beforeEach(function() { + return (this.req.url = '/12345/foo//output.pdf') + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a non-project file', function() { + beforeEach(function() { + return (this.req.url = '/.foo/output.pdf') + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a file outside the compiledir', function() { + beforeEach(function() { + return (this.req.url = '/../bar/output.pdf') + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a file with no leading /', function() { + beforeEach(function() { + return (this.req.url = './../bar/output.pdf') + }) + + return it('should send a 404', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(404) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) + + describe('with a github style path', function() { + beforeEach(function() { + this.req.url = '/henryoswald-latex_example/output/output.log' + return (this.fs.realpath = sinon + .stub() + .callsArgWith( + 1, + null, + `${this.settings.path.compilesDir}/henryoswald-latex_example/output/output.log` + )) + }) + + return it('should call next', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(200) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res, done) + }) + }) + + return describe('with an error from fs.realpath', function() { + beforeEach(function() { + return (this.fs.realpath = sinon.stub().callsArgWith(1, 'error')) + }) + + return it('should send a 500', function(done) { + this.res.sendStatus = function(resCode) { + resCode.should.equal(500) + return done() + } + return this.StaticServerForbidSymlinks(this.req, this.res) + }) + }) +}) diff --git a/test/unit/js/TikzManager.js b/test/unit/js/TikzManager.js index f35d2619..1a9874cb 100644 --- a/test/unit/js/TikzManager.js +++ b/test/unit/js/TikzManager.js @@ -8,148 +8,180 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/TikzManager'); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/TikzManager' +) describe('TikzManager', function() { - beforeEach(function() { - return this.TikzManager = SandboxedModule.require(modulePath, { requires: { - "./ResourceWriter": (this.ResourceWriter = {}), - "./SafeReader": (this.SafeReader = {}), - "fs": (this.fs = {}), - "logger-sharelatex": (this.logger = {log() {}}) - } - });}); - - describe("checkMainFile", function() { - beforeEach(function() { - this.compileDir = "compile-dir"; - this.mainFile = "main.tex"; - return this.callback = sinon.stub(); - }); - - describe("if there is already an output.tex file in the resources", function() { - beforeEach(function() { - this.resources = [{path:"main.tex"},{path:"output.tex"}]; - return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); - }); - - return it("should call the callback with false ", function() { - return this.callback.calledWithExactly(null, false) - .should.equal(true); - }); - }); - - return describe("if there is no output.tex file in the resources", function() { - beforeEach(function() { - this.resources = [{path:"main.tex"}]; - return this.ResourceWriter.checkPath = sinon.stub() - .withArgs(this.compileDir, this.mainFile) - .callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`); - }); - - describe("and the main file contains tikzexternalize", function() { - beforeEach(function() { - this.SafeReader.readFile = sinon.stub() - .withArgs(`${this.compileDir}/${this.mainFile}`) - .callsArgWith(3, null, "hello \\tikzexternalize"); - return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); - }); - - it("should look at the file on disk", function() { - return this.SafeReader.readFile - .calledWith(`${this.compileDir}/${this.mainFile}`) - .should.equal(true); - }); - - return it("should call the callback with true ", function() { - return this.callback.calledWithExactly(null, true) - .should.equal(true); - }); - }); - - describe("and the main file does not contain tikzexternalize", function() { - beforeEach(function() { - this.SafeReader.readFile = sinon.stub() - .withArgs(`${this.compileDir}/${this.mainFile}`) - .callsArgWith(3, null, "hello"); - return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); - }); - - it("should look at the file on disk", function() { - return this.SafeReader.readFile - .calledWith(`${this.compileDir}/${this.mainFile}`) - .should.equal(true); - }); - - return it("should call the callback with false", function() { - return this.callback.calledWithExactly(null, false) - .should.equal(true); - }); - }); - - return describe("and the main file contains \\usepackage{pstool}", function() { - beforeEach(function() { - this.SafeReader.readFile = sinon.stub() - .withArgs(`${this.compileDir}/${this.mainFile}`) - .callsArgWith(3, null, "hello \\usepackage[random-options]{pstool}"); - return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback); - }); - - it("should look at the file on disk", function() { - return this.SafeReader.readFile - .calledWith(`${this.compileDir}/${this.mainFile}`) - .should.equal(true); - }); - - return it("should call the callback with true ", function() { - return this.callback.calledWithExactly(null, true) - .should.equal(true); - }); - }); - }); - }); - - return describe("injectOutputFile", function() { - beforeEach(function() { - this.rootDir = "/mock"; - this.filename = "filename.tex"; - this.callback = sinon.stub(); - this.content = `\ + beforeEach(function() { + return (this.TikzManager = SandboxedModule.require(modulePath, { + requires: { + './ResourceWriter': (this.ResourceWriter = {}), + './SafeReader': (this.SafeReader = {}), + fs: (this.fs = {}), + 'logger-sharelatex': (this.logger = { log() {} }) + } + })) + }) + + describe('checkMainFile', function() { + beforeEach(function() { + this.compileDir = 'compile-dir' + this.mainFile = 'main.tex' + return (this.callback = sinon.stub()) + }) + + describe('if there is already an output.tex file in the resources', function() { + beforeEach(function() { + this.resources = [{ path: 'main.tex' }, { path: 'output.tex' }] + return this.TikzManager.checkMainFile( + this.compileDir, + this.mainFile, + this.resources, + this.callback + ) + }) + + return it('should call the callback with false ', function() { + return this.callback.calledWithExactly(null, false).should.equal(true) + }) + }) + + return describe('if there is no output.tex file in the resources', function() { + beforeEach(function() { + this.resources = [{ path: 'main.tex' }] + return (this.ResourceWriter.checkPath = sinon + .stub() + .withArgs(this.compileDir, this.mainFile) + .callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`)) + }) + + describe('and the main file contains tikzexternalize', function() { + beforeEach(function() { + this.SafeReader.readFile = sinon + .stub() + .withArgs(`${this.compileDir}/${this.mainFile}`) + .callsArgWith(3, null, 'hello \\tikzexternalize') + return this.TikzManager.checkMainFile( + this.compileDir, + this.mainFile, + this.resources, + this.callback + ) + }) + + it('should look at the file on disk', function() { + return this.SafeReader.readFile + .calledWith(`${this.compileDir}/${this.mainFile}`) + .should.equal(true) + }) + + return it('should call the callback with true ', function() { + return this.callback.calledWithExactly(null, true).should.equal(true) + }) + }) + + describe('and the main file does not contain tikzexternalize', function() { + beforeEach(function() { + this.SafeReader.readFile = sinon + .stub() + .withArgs(`${this.compileDir}/${this.mainFile}`) + .callsArgWith(3, null, 'hello') + return this.TikzManager.checkMainFile( + this.compileDir, + this.mainFile, + this.resources, + this.callback + ) + }) + + it('should look at the file on disk', function() { + return this.SafeReader.readFile + .calledWith(`${this.compileDir}/${this.mainFile}`) + .should.equal(true) + }) + + return it('should call the callback with false', function() { + return this.callback.calledWithExactly(null, false).should.equal(true) + }) + }) + + return describe('and the main file contains \\usepackage{pstool}', function() { + beforeEach(function() { + this.SafeReader.readFile = sinon + .stub() + .withArgs(`${this.compileDir}/${this.mainFile}`) + .callsArgWith(3, null, 'hello \\usepackage[random-options]{pstool}') + return this.TikzManager.checkMainFile( + this.compileDir, + this.mainFile, + this.resources, + this.callback + ) + }) + + it('should look at the file on disk', function() { + return this.SafeReader.readFile + .calledWith(`${this.compileDir}/${this.mainFile}`) + .should.equal(true) + }) + + return it('should call the callback with true ', function() { + return this.callback.calledWithExactly(null, true).should.equal(true) + }) + }) + }) + }) + + return describe('injectOutputFile', function() { + beforeEach(function() { + this.rootDir = '/mock' + this.filename = 'filename.tex' + this.callback = sinon.stub() + this.content = `\ \\documentclass{article} \\usepackage{tikz} \\tikzexternalize \\begin{document} Hello world \\end{document}\ -`; - this.fs.readFile = sinon.stub().callsArgWith(2, null, this.content); - this.fs.writeFile = sinon.stub().callsArg(3); - this.ResourceWriter.checkPath = sinon.stub().callsArgWith(2, null, `${this.rootDir}/${this.filename}`); - return this.TikzManager.injectOutputFile(this.rootDir, this.filename, this.callback); - }); - - it("sould check the path", function() { - return this.ResourceWriter.checkPath.calledWith(this.rootDir, this.filename) - .should.equal(true); - }); - - it("should read the file", function() { - return this.fs.readFile - .calledWith(`${this.rootDir}/${this.filename}`, "utf8") - .should.equal(true); - }); - - it("should write out the same file as output.tex", function() { - return this.fs.writeFile - .calledWith(`${this.rootDir}/output.tex`, this.content, {flag: 'wx'}) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); -}); +` + this.fs.readFile = sinon.stub().callsArgWith(2, null, this.content) + this.fs.writeFile = sinon.stub().callsArg(3) + this.ResourceWriter.checkPath = sinon + .stub() + .callsArgWith(2, null, `${this.rootDir}/${this.filename}`) + return this.TikzManager.injectOutputFile( + this.rootDir, + this.filename, + this.callback + ) + }) + + it('sould check the path', function() { + return this.ResourceWriter.checkPath + .calledWith(this.rootDir, this.filename) + .should.equal(true) + }) + + it('should read the file', function() { + return this.fs.readFile + .calledWith(`${this.rootDir}/${this.filename}`, 'utf8') + .should.equal(true) + }) + + it('should write out the same file as output.tex', function() { + return this.fs.writeFile + .calledWith(`${this.rootDir}/output.tex`, this.content, { flag: 'wx' }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index 7f024507..f056a6eb 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -10,259 +10,347 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/UrlCache'); -const { EventEmitter } = require("events"); - -describe("UrlCache", function() { - beforeEach(function() { - this.callback = sinon.stub(); - this.url = "www.example.com/file"; - this.project_id = "project-id-123"; - return this.UrlCache = SandboxedModule.require(modulePath, { requires: { - "./db" : {}, - "./UrlFetcher" : (this.UrlFetcher = {}), - "logger-sharelatex": (this.logger = {log: sinon.stub()}), - "settings-sharelatex": (this.Settings = { path: {clsiCacheDir: "/cache/dir"} }), - "fs": (this.fs = {}) - } - });}); - - describe("_doesUrlNeedDownloading", function() { - beforeEach(function() { - this.lastModified = new Date(); - return this.lastModifiedRoundedToSeconds = new Date(Math.floor(this.lastModified.getTime() / 1000) * 1000); - }); - - describe("when URL does not exist in cache", function() { - beforeEach(function() { - this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, null); - return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); - }); - - return it("should return the callback with true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - - return describe("when URL does exist in cache", function() { - beforeEach(function() { - this.urlDetails = {}; - return this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, this.urlDetails); - }); - - describe("when the modified date is more recent than the cached modified date", function() { - beforeEach(function() { - this.urlDetails.lastModified = new Date(this.lastModified.getTime() - 1000); - return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); - }); - - it("should get the url details", function() { - return this.UrlCache._findUrlDetails - .calledWith(this.project_id, this.url) - .should.equal(true); - }); - - return it("should return the callback with true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - - describe("when the cached modified date is more recent than the modified date", function() { - beforeEach(function() { - this.urlDetails.lastModified = new Date(this.lastModified.getTime() + 1000); - return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); - }); - - return it("should return the callback with false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); - - describe("when the cached modified date is equal to the modified date", function() { - beforeEach(function() { - this.urlDetails.lastModified = this.lastModified; - return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); - }); - - return it("should return the callback with false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); - - describe("when the provided modified date does not exist", function() { - beforeEach(function() { - this.lastModified = null; - return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); - }); - - return it("should return the callback with true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - - return describe("when the URL does not have a modified date", function() { - beforeEach(function() { - this.urlDetails.lastModified = null; - return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback); - }); - - return it("should return the callback with true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - }); - }); - - describe("_ensureUrlIsInCache", function() { - beforeEach(function() { - this.UrlFetcher.pipeUrlToFile = sinon.stub().callsArg(2); - return this.UrlCache._updateOrCreateUrlDetails = sinon.stub().callsArg(3); - }); - - describe("when the URL needs updating", function() { - beforeEach(function() { - this.UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, true); - return this.UrlCache._ensureUrlIsInCache(this.project_id, this.url, this.lastModified, this.callback); - }); - - it("should check that the url needs downloading", function() { - return this.UrlCache._doesUrlNeedDownloading - .calledWith(this.project_id, this.url, this.lastModifiedRoundedToSeconds) - .should.equal(true); - }); - - it("should download the URL to the cache file", function() { - return this.UrlFetcher.pipeUrlToFile - .calledWith(this.url, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) - .should.equal(true); - }); - - - it("should update the database entry", function() { - return this.UrlCache._updateOrCreateUrlDetails - .calledWith(this.project_id, this.url, this.lastModifiedRoundedToSeconds) - .should.equal(true); - }); - - return it("should return the callback with the cache file path", function() { - return this.callback - .calledWith(null, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) - .should.equal(true); - }); - }); - - return describe("when the URL does not need updating", function() { - beforeEach(function() { - this.UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, false); - return this.UrlCache._ensureUrlIsInCache(this.project_id, this.url, this.lastModified, this.callback); - }); - - it("should not download the URL to the cache file", function() { - return this.UrlFetcher.pipeUrlToFile - .called.should.equal(false); - }); - - return it("should return the callback with the cache file path", function() { - return this.callback - .calledWith(null, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) - .should.equal(true); - }); - }); - }); - - describe("downloadUrlToFile", function() { - beforeEach(function() { - this.cachePath = "path/to/cached/url"; - this.destPath = "path/to/destination"; - this.UrlCache._copyFile = sinon.stub().callsArg(2); - this.UrlCache._ensureUrlIsInCache = sinon.stub().callsArgWith(3, null, this.cachePath); - return this.UrlCache.downloadUrlToFile(this.project_id, this.url, this.destPath, this.lastModified, this.callback); - }); - - it("should ensure the URL is downloaded and updated in the cache", function() { - return this.UrlCache._ensureUrlIsInCache - .calledWith(this.project_id, this.url, this.lastModified) - .should.equal(true); - }); - - it("should copy the file to the new location", function() { - return this.UrlCache._copyFile - .calledWith(this.cachePath, this.destPath) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("_deleteUrlCacheFromDisk", function() { - beforeEach(function() { - this.fs.unlink = sinon.stub().callsArg(1); - return this.UrlCache._deleteUrlCacheFromDisk(this.project_id, this.url, this.callback); - }); - - it("should delete the cache file", function() { - return this.fs.unlink - .calledWith(this.UrlCache._cacheFilePathForUrl(this.project_id, this.url)) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("_clearUrlFromCache", function() { - beforeEach(function() { - this.UrlCache._deleteUrlCacheFromDisk = sinon.stub().callsArg(2); - this.UrlCache._clearUrlDetails = sinon.stub().callsArg(2); - return this.UrlCache._clearUrlFromCache(this.project_id, this.url, this.callback); - }); - - it("should delete the file on disk", function() { - return this.UrlCache._deleteUrlCacheFromDisk - .calledWith(this.project_id, this.url) - .should.equal(true); - }); - - it("should clear the entry in the database", function() { - return this.UrlCache._clearUrlDetails - .calledWith(this.project_id, this.url) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("clearProject", function() { - beforeEach(function() { - this.urls = [ - "www.example.com/file1", - "www.example.com/file2" - ]; - this.UrlCache._findAllUrlsInProject = sinon.stub().callsArgWith(1, null, this.urls); - this.UrlCache._clearUrlFromCache = sinon.stub().callsArg(2); - return this.UrlCache.clearProject(this.project_id, this.callback); - }); - - it("should clear the cache for each url in the project", function() { - return Array.from(this.urls).map((url) => - this.UrlCache._clearUrlFromCache - .calledWith(this.project_id, url) - .should.equal(true)); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); -}); - - - +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join(__dirname, '../../../app/js/UrlCache') +const { EventEmitter } = require('events') + +describe('UrlCache', function() { + beforeEach(function() { + this.callback = sinon.stub() + this.url = 'www.example.com/file' + this.project_id = 'project-id-123' + return (this.UrlCache = SandboxedModule.require(modulePath, { + requires: { + './db': {}, + './UrlFetcher': (this.UrlFetcher = {}), + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + 'settings-sharelatex': (this.Settings = { + path: { clsiCacheDir: '/cache/dir' } + }), + fs: (this.fs = {}) + } + })) + }) + + describe('_doesUrlNeedDownloading', function() { + beforeEach(function() { + this.lastModified = new Date() + return (this.lastModifiedRoundedToSeconds = new Date( + Math.floor(this.lastModified.getTime() / 1000) * 1000 + )) + }) + + describe('when URL does not exist in cache', function() { + beforeEach(function() { + this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, null) + return this.UrlCache._doesUrlNeedDownloading( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + return it('should return the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + return describe('when URL does exist in cache', function() { + beforeEach(function() { + this.urlDetails = {} + return (this.UrlCache._findUrlDetails = sinon + .stub() + .callsArgWith(2, null, this.urlDetails)) + }) + + describe('when the modified date is more recent than the cached modified date', function() { + beforeEach(function() { + this.urlDetails.lastModified = new Date( + this.lastModified.getTime() - 1000 + ) + return this.UrlCache._doesUrlNeedDownloading( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + it('should get the url details', function() { + return this.UrlCache._findUrlDetails + .calledWith(this.project_id, this.url) + .should.equal(true) + }) + + return it('should return the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + describe('when the cached modified date is more recent than the modified date', function() { + beforeEach(function() { + this.urlDetails.lastModified = new Date( + this.lastModified.getTime() + 1000 + ) + return this.UrlCache._doesUrlNeedDownloading( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + return it('should return the callback with false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + + describe('when the cached modified date is equal to the modified date', function() { + beforeEach(function() { + this.urlDetails.lastModified = this.lastModified + return this.UrlCache._doesUrlNeedDownloading( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + return it('should return the callback with false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + + describe('when the provided modified date does not exist', function() { + beforeEach(function() { + this.lastModified = null + return this.UrlCache._doesUrlNeedDownloading( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + return it('should return the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + return describe('when the URL does not have a modified date', function() { + beforeEach(function() { + this.urlDetails.lastModified = null + return this.UrlCache._doesUrlNeedDownloading( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + return it('should return the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + }) + }) + + describe('_ensureUrlIsInCache', function() { + beforeEach(function() { + this.UrlFetcher.pipeUrlToFile = sinon.stub().callsArg(2) + return (this.UrlCache._updateOrCreateUrlDetails = sinon + .stub() + .callsArg(3)) + }) + + describe('when the URL needs updating', function() { + beforeEach(function() { + this.UrlCache._doesUrlNeedDownloading = sinon + .stub() + .callsArgWith(3, null, true) + return this.UrlCache._ensureUrlIsInCache( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + it('should check that the url needs downloading', function() { + return this.UrlCache._doesUrlNeedDownloading + .calledWith( + this.project_id, + this.url, + this.lastModifiedRoundedToSeconds + ) + .should.equal(true) + }) + + it('should download the URL to the cache file', function() { + return this.UrlFetcher.pipeUrlToFile + .calledWith( + this.url, + this.UrlCache._cacheFilePathForUrl(this.project_id, this.url) + ) + .should.equal(true) + }) + + it('should update the database entry', function() { + return this.UrlCache._updateOrCreateUrlDetails + .calledWith( + this.project_id, + this.url, + this.lastModifiedRoundedToSeconds + ) + .should.equal(true) + }) + + return it('should return the callback with the cache file path', function() { + return this.callback + .calledWith( + null, + this.UrlCache._cacheFilePathForUrl(this.project_id, this.url) + ) + .should.equal(true) + }) + }) + + return describe('when the URL does not need updating', function() { + beforeEach(function() { + this.UrlCache._doesUrlNeedDownloading = sinon + .stub() + .callsArgWith(3, null, false) + return this.UrlCache._ensureUrlIsInCache( + this.project_id, + this.url, + this.lastModified, + this.callback + ) + }) + + it('should not download the URL to the cache file', function() { + return this.UrlFetcher.pipeUrlToFile.called.should.equal(false) + }) + + return it('should return the callback with the cache file path', function() { + return this.callback + .calledWith( + null, + this.UrlCache._cacheFilePathForUrl(this.project_id, this.url) + ) + .should.equal(true) + }) + }) + }) + + describe('downloadUrlToFile', function() { + beforeEach(function() { + this.cachePath = 'path/to/cached/url' + this.destPath = 'path/to/destination' + this.UrlCache._copyFile = sinon.stub().callsArg(2) + this.UrlCache._ensureUrlIsInCache = sinon + .stub() + .callsArgWith(3, null, this.cachePath) + return this.UrlCache.downloadUrlToFile( + this.project_id, + this.url, + this.destPath, + this.lastModified, + this.callback + ) + }) + + it('should ensure the URL is downloaded and updated in the cache', function() { + return this.UrlCache._ensureUrlIsInCache + .calledWith(this.project_id, this.url, this.lastModified) + .should.equal(true) + }) + + it('should copy the file to the new location', function() { + return this.UrlCache._copyFile + .calledWith(this.cachePath, this.destPath) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('_deleteUrlCacheFromDisk', function() { + beforeEach(function() { + this.fs.unlink = sinon.stub().callsArg(1) + return this.UrlCache._deleteUrlCacheFromDisk( + this.project_id, + this.url, + this.callback + ) + }) + + it('should delete the cache file', function() { + return this.fs.unlink + .calledWith( + this.UrlCache._cacheFilePathForUrl(this.project_id, this.url) + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('_clearUrlFromCache', function() { + beforeEach(function() { + this.UrlCache._deleteUrlCacheFromDisk = sinon.stub().callsArg(2) + this.UrlCache._clearUrlDetails = sinon.stub().callsArg(2) + return this.UrlCache._clearUrlFromCache( + this.project_id, + this.url, + this.callback + ) + }) + + it('should delete the file on disk', function() { + return this.UrlCache._deleteUrlCacheFromDisk + .calledWith(this.project_id, this.url) + .should.equal(true) + }) + + it('should clear the entry in the database', function() { + return this.UrlCache._clearUrlDetails + .calledWith(this.project_id, this.url) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('clearProject', function() { + beforeEach(function() { + this.urls = ['www.example.com/file1', 'www.example.com/file2'] + this.UrlCache._findAllUrlsInProject = sinon + .stub() + .callsArgWith(1, null, this.urls) + this.UrlCache._clearUrlFromCache = sinon.stub().callsArg(2) + return this.UrlCache.clearProject(this.project_id, this.callback) + }) + + it('should clear the cache for each url in the project', function() { + return Array.from(this.urls).map(url => + this.UrlCache._clearUrlFromCache + .calledWith(this.project_id, url) + .should.equal(true) + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index 453a3867..e5ce52b9 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -8,152 +8,165 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/UrlFetcher'); -const { EventEmitter } = require("events"); - -describe("UrlFetcher", function() { - beforeEach(function() { - this.callback = sinon.stub(); - this.url = "https://www.example.com/file/here?query=string"; - return this.UrlFetcher = SandboxedModule.require(modulePath, { requires: { - request: { defaults: (this.defaults = sinon.stub().returns(this.request = {})) - }, - fs: (this.fs = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "settings-sharelatex": (this.settings = {}) - } - });}); - - it("should turn off the cookie jar in request", function() { - return this.defaults.calledWith({jar: false}) - .should.equal(true); - }); - - describe("rewrite url domain if filestoreDomainOveride is set", function() { - beforeEach(function() { - this.path = "/path/to/file/on/disk"; - this.request.get = sinon.stub().returns(this.urlStream = new EventEmitter); - this.urlStream.pipe = sinon.stub(); - this.urlStream.pause = sinon.stub(); - this.urlStream.resume = sinon.stub(); - this.fs.createWriteStream = sinon.stub().returns(this.fileStream = new EventEmitter); - return this.fs.unlink = (file, callback) => callback(); - }); - - it("should use the normal domain when override not set", function(done){ - this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { - this.request.get.args[0][0].url.should.equal(this.url); - return done(); - }); - this.res = {statusCode: 200}; - this.urlStream.emit("response", this.res); - this.urlStream.emit("end"); - return this.fileStream.emit("finish"); - }); - - - return it("should use override domain when filestoreDomainOveride is set", function(done){ - this.settings.filestoreDomainOveride = "192.11.11.11"; - this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { - this.request.get.args[0][0].url.should.equal("192.11.11.11/file/here?query=string"); - return done(); - }); - this.res = {statusCode: 200}; - this.urlStream.emit("response", this.res); - this.urlStream.emit("end"); - return this.fileStream.emit("finish"); - }); - }); - - return describe("pipeUrlToFile", function() { - beforeEach(function(done){ - this.path = "/path/to/file/on/disk"; - this.request.get = sinon.stub().returns(this.urlStream = new EventEmitter); - this.urlStream.pipe = sinon.stub(); - this.urlStream.pause = sinon.stub(); - this.urlStream.resume = sinon.stub(); - this.fs.createWriteStream = sinon.stub().returns(this.fileStream = new EventEmitter); - this.fs.unlink = (file, callback) => callback(); - return done(); - }); - - describe("successfully", function() { - beforeEach(function(done){ - this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { - this.callback(); - return done(); - }); - this.res = {statusCode: 200}; - this.urlStream.emit("response", this.res); - this.urlStream.emit("end"); - return this.fileStream.emit("finish"); - }); - - - it("should request the URL", function() { - return this.request.get - .calledWith(sinon.match({"url": this.url})) - .should.equal(true); - }); - - it("should open the file for writing", function() { - return this.fs.createWriteStream - .calledWith(this.path) - .should.equal(true); - }); - - it("should pipe the URL to the file", function() { - return this.urlStream.pipe - .calledWith(this.fileStream) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("with non success status code", function() { - beforeEach(function(done){ - this.UrlFetcher.pipeUrlToFile(this.url, this.path, err=> { - this.callback(err); - return done(); - }); - this.res = {statusCode: 404}; - this.urlStream.emit("response", this.res); - return this.urlStream.emit("end"); - }); - - return it("should call the callback with an error", function() { - return this.callback - .calledWith(new Error("URL returned non-success status code: 404")) - .should.equal(true); - }); - }); - - return describe("with error", function() { - beforeEach(function(done){ - this.UrlFetcher.pipeUrlToFile(this.url, this.path, err=> { - this.callback(err); - return done(); - }); - return this.urlStream.emit("error", (this.error = new Error("something went wrong"))); - }); - - it("should call the callback with the error", function() { - return this.callback - .calledWith(this.error) - .should.equal(true); - }); - - return it("should only call the callback once, even if end is called", function() { - this.urlStream.emit("end"); - return this.callback.calledOnce.should.equal(true); - }); - }); - }); -}); - +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join(__dirname, '../../../app/js/UrlFetcher') +const { EventEmitter } = require('events') + +describe('UrlFetcher', function() { + beforeEach(function() { + this.callback = sinon.stub() + this.url = 'https://www.example.com/file/here?query=string' + return (this.UrlFetcher = SandboxedModule.require(modulePath, { + requires: { + request: { + defaults: (this.defaults = sinon.stub().returns((this.request = {}))) + }, + fs: (this.fs = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + 'settings-sharelatex': (this.settings = {}) + } + })) + }) + + it('should turn off the cookie jar in request', function() { + return this.defaults.calledWith({ jar: false }).should.equal(true) + }) + + describe('rewrite url domain if filestoreDomainOveride is set', function() { + beforeEach(function() { + this.path = '/path/to/file/on/disk' + this.request.get = sinon + .stub() + .returns((this.urlStream = new EventEmitter())) + this.urlStream.pipe = sinon.stub() + this.urlStream.pause = sinon.stub() + this.urlStream.resume = sinon.stub() + this.fs.createWriteStream = sinon + .stub() + .returns((this.fileStream = new EventEmitter())) + return (this.fs.unlink = (file, callback) => callback()) + }) + + it('should use the normal domain when override not set', function(done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.request.get.args[0][0].url.should.equal(this.url) + return done() + }) + this.res = { statusCode: 200 } + this.urlStream.emit('response', this.res) + this.urlStream.emit('end') + return this.fileStream.emit('finish') + }) + + return it('should use override domain when filestoreDomainOveride is set', function(done) { + this.settings.filestoreDomainOveride = '192.11.11.11' + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.request.get.args[0][0].url.should.equal( + '192.11.11.11/file/here?query=string' + ) + return done() + }) + this.res = { statusCode: 200 } + this.urlStream.emit('response', this.res) + this.urlStream.emit('end') + return this.fileStream.emit('finish') + }) + }) + + return describe('pipeUrlToFile', function() { + beforeEach(function(done) { + this.path = '/path/to/file/on/disk' + this.request.get = sinon + .stub() + .returns((this.urlStream = new EventEmitter())) + this.urlStream.pipe = sinon.stub() + this.urlStream.pause = sinon.stub() + this.urlStream.resume = sinon.stub() + this.fs.createWriteStream = sinon + .stub() + .returns((this.fileStream = new EventEmitter())) + this.fs.unlink = (file, callback) => callback() + return done() + }) + + describe('successfully', function() { + beforeEach(function(done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.callback() + return done() + }) + this.res = { statusCode: 200 } + this.urlStream.emit('response', this.res) + this.urlStream.emit('end') + return this.fileStream.emit('finish') + }) + + it('should request the URL', function() { + return this.request.get + .calledWith(sinon.match({ url: this.url })) + .should.equal(true) + }) + + it('should open the file for writing', function() { + return this.fs.createWriteStream + .calledWith(this.path) + .should.equal(true) + }) + + it('should pipe the URL to the file', function() { + return this.urlStream.pipe + .calledWith(this.fileStream) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('with non success status code', function() { + beforeEach(function(done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { + this.callback(err) + return done() + }) + this.res = { statusCode: 404 } + this.urlStream.emit('response', this.res) + return this.urlStream.emit('end') + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWith(new Error('URL returned non-success status code: 404')) + .should.equal(true) + }) + }) + + return describe('with error', function() { + beforeEach(function(done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { + this.callback(err) + return done() + }) + return this.urlStream.emit( + 'error', + (this.error = new Error('something went wrong')) + ) + }) + + it('should call the callback with the error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + + return it('should only call the callback once, even if end is called', function() { + this.urlStream.emit('end') + return this.callback.calledOnce.should.equal(true) + }) + }) + }) +}) From a2a3fddd5474b075303c45f78a16031684871de2 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:15:54 +0100 Subject: [PATCH 480/709] decaffeinate: Rename BrokenLatexFileTests.coffee and 9 other files from .coffee to .js --- .../{BrokenLatexFileTests.coffee => BrokenLatexFileTests.js} | 0 .../coffee/{DeleteOldFilesTest.coffee => DeleteOldFilesTest.js} | 0 .../{ExampleDocumentTests.coffee => ExampleDocumentTests.js} | 0 .../{SimpleLatexFileTests.coffee => SimpleLatexFileTests.js} | 0 test/acceptance/coffee/{SynctexTests.coffee => SynctexTests.js} | 0 test/acceptance/coffee/{TimeoutTests.coffee => TimeoutTests.js} | 0 .../coffee/{UrlCachingTests.coffee => UrlCachingTests.js} | 0 .../coffee/{WordcountTests.coffee => WordcountTests.js} | 0 test/acceptance/coffee/helpers/{Client.coffee => Client.js} | 0 test/acceptance/coffee/helpers/{ClsiApp.coffee => ClsiApp.js} | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename test/acceptance/coffee/{BrokenLatexFileTests.coffee => BrokenLatexFileTests.js} (100%) rename test/acceptance/coffee/{DeleteOldFilesTest.coffee => DeleteOldFilesTest.js} (100%) rename test/acceptance/coffee/{ExampleDocumentTests.coffee => ExampleDocumentTests.js} (100%) rename test/acceptance/coffee/{SimpleLatexFileTests.coffee => SimpleLatexFileTests.js} (100%) rename test/acceptance/coffee/{SynctexTests.coffee => SynctexTests.js} (100%) rename test/acceptance/coffee/{TimeoutTests.coffee => TimeoutTests.js} (100%) rename test/acceptance/coffee/{UrlCachingTests.coffee => UrlCachingTests.js} (100%) rename test/acceptance/coffee/{WordcountTests.coffee => WordcountTests.js} (100%) rename test/acceptance/coffee/helpers/{Client.coffee => Client.js} (100%) rename test/acceptance/coffee/helpers/{ClsiApp.coffee => ClsiApp.js} (100%) diff --git a/test/acceptance/coffee/BrokenLatexFileTests.coffee b/test/acceptance/coffee/BrokenLatexFileTests.js similarity index 100% rename from test/acceptance/coffee/BrokenLatexFileTests.coffee rename to test/acceptance/coffee/BrokenLatexFileTests.js diff --git a/test/acceptance/coffee/DeleteOldFilesTest.coffee b/test/acceptance/coffee/DeleteOldFilesTest.js similarity index 100% rename from test/acceptance/coffee/DeleteOldFilesTest.coffee rename to test/acceptance/coffee/DeleteOldFilesTest.js diff --git a/test/acceptance/coffee/ExampleDocumentTests.coffee b/test/acceptance/coffee/ExampleDocumentTests.js similarity index 100% rename from test/acceptance/coffee/ExampleDocumentTests.coffee rename to test/acceptance/coffee/ExampleDocumentTests.js diff --git a/test/acceptance/coffee/SimpleLatexFileTests.coffee b/test/acceptance/coffee/SimpleLatexFileTests.js similarity index 100% rename from test/acceptance/coffee/SimpleLatexFileTests.coffee rename to test/acceptance/coffee/SimpleLatexFileTests.js diff --git a/test/acceptance/coffee/SynctexTests.coffee b/test/acceptance/coffee/SynctexTests.js similarity index 100% rename from test/acceptance/coffee/SynctexTests.coffee rename to test/acceptance/coffee/SynctexTests.js diff --git a/test/acceptance/coffee/TimeoutTests.coffee b/test/acceptance/coffee/TimeoutTests.js similarity index 100% rename from test/acceptance/coffee/TimeoutTests.coffee rename to test/acceptance/coffee/TimeoutTests.js diff --git a/test/acceptance/coffee/UrlCachingTests.coffee b/test/acceptance/coffee/UrlCachingTests.js similarity index 100% rename from test/acceptance/coffee/UrlCachingTests.coffee rename to test/acceptance/coffee/UrlCachingTests.js diff --git a/test/acceptance/coffee/WordcountTests.coffee b/test/acceptance/coffee/WordcountTests.js similarity index 100% rename from test/acceptance/coffee/WordcountTests.coffee rename to test/acceptance/coffee/WordcountTests.js diff --git a/test/acceptance/coffee/helpers/Client.coffee b/test/acceptance/coffee/helpers/Client.js similarity index 100% rename from test/acceptance/coffee/helpers/Client.coffee rename to test/acceptance/coffee/helpers/Client.js diff --git a/test/acceptance/coffee/helpers/ClsiApp.coffee b/test/acceptance/coffee/helpers/ClsiApp.js similarity index 100% rename from test/acceptance/coffee/helpers/ClsiApp.coffee rename to test/acceptance/coffee/helpers/ClsiApp.js From 955749a7c410895fadb34edf88a929ae754c8356 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:16:00 +0100 Subject: [PATCH 481/709] decaffeinate: Convert BrokenLatexFileTests.coffee and 9 other files to JS --- .../acceptance/coffee/BrokenLatexFileTests.js | 100 +++-- test/acceptance/coffee/DeleteOldFilesTest.js | 77 ++-- .../acceptance/coffee/ExampleDocumentTests.js | 307 +++++++------ .../acceptance/coffee/SimpleLatexFileTests.js | 84 ++-- test/acceptance/coffee/SynctexTests.js | 87 ++-- test/acceptance/coffee/TimeoutTests.js | 70 +-- test/acceptance/coffee/UrlCachingTests.js | 418 ++++++++++-------- test/acceptance/coffee/WordcountTests.js | 76 ++-- test/acceptance/coffee/helpers/Client.js | 206 +++++---- test/acceptance/coffee/helpers/ClsiApp.js | 66 ++- 10 files changed, 884 insertions(+), 607 deletions(-) diff --git a/test/acceptance/coffee/BrokenLatexFileTests.js b/test/acceptance/coffee/BrokenLatexFileTests.js index 8ab4344f..5aea6257 100644 --- a/test/acceptance/coffee/BrokenLatexFileTests.js +++ b/test/acceptance/coffee/BrokenLatexFileTests.js @@ -1,48 +1,70 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const ClsiApp = require("./helpers/ClsiApp"); -describe "Broken LaTeX file", -> - before (done)-> - @broken_request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{articl % :( - \\begin{documen % :( - Broken - \\end{documen % :( - ''' +describe("Broken LaTeX file", function() { + before(function(done){ + this.broken_request = { + resources: [{ + path: "main.tex", + content: `\ +\\documentclass{articl % :( +\\begin{documen % :( +Broken +\\end{documen % :(\ +` + } ] - @correct_request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' + }; + this.correct_request = { + resources: [{ + path: "main.tex", + content: `\ +\\documentclass{article} +\\begin{document} +Hello world +\\end{document}\ +` + } ] - ClsiApp.ensureRunning done + }; + return ClsiApp.ensureRunning(done); + }); - describe "on first run", -> - before (done) -> - @project_id = Client.randomId() - Client.compile @project_id, @broken_request, (@error, @res, @body) => done() + describe("on first run", function() { + before(function(done) { + this.project_id = Client.randomId(); + return Client.compile(this.project_id, this.broken_request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); + }); - it "should return a failure status", -> - @body.compile.status.should.equal "failure" + return it("should return a failure status", function() { + return this.body.compile.status.should.equal("failure"); + }); + }); - describe "on second run", -> - before (done) -> - @project_id = Client.randomId() - Client.compile @project_id, @correct_request, () => - Client.compile @project_id, @broken_request, (@error, @res, @body) => - done() + return describe("on second run", function() { + before(function(done) { + this.project_id = Client.randomId(); + return Client.compile(this.project_id, this.correct_request, () => { + return Client.compile(this.project_id, this.broken_request, (error, res, body) => { + this.error = error; + this.res = res; + this.body = body; + return done(); + }); + }); + }); - it "should return a failure status", -> - @body.compile.status.should.equal "failure" + return it("should return a failure status", function() { + return this.body.compile.status.should.equal("failure"); + }); + }); +}); diff --git a/test/acceptance/coffee/DeleteOldFilesTest.js b/test/acceptance/coffee/DeleteOldFilesTest.js index 1cb67765..d6958c20 100644 --- a/test/acceptance/coffee/DeleteOldFilesTest.js +++ b/test/acceptance/coffee/DeleteOldFilesTest.js @@ -1,36 +1,55 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const ClsiApp = require("./helpers/ClsiApp"); -describe "Deleting Old Files", -> - before (done)-> - @request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' +describe("Deleting Old Files", function() { + before(function(done){ + this.request = { + resources: [{ + path: "main.tex", + content: `\ +\\documentclass{article} +\\begin{document} +Hello world +\\end{document}\ +` + } ] - ClsiApp.ensureRunning done + }; + return ClsiApp.ensureRunning(done); + }); - describe "on first run", -> - before (done) -> - @project_id = Client.randomId() - Client.compile @project_id, @request, (@error, @res, @body) => done() + return describe("on first run", function() { + before(function(done) { + this.project_id = Client.randomId(); + return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); + }); - it "should return a success status", -> - @body.compile.status.should.equal "success" + it("should return a success status", function() { + return this.body.compile.status.should.equal("success"); + }); - describe "after file has been deleted", -> - before (done) -> - @request.resources = [] - Client.compile @project_id, @request, (@error, @res, @body) => - done() + return describe("after file has been deleted", function() { + before(function(done) { + this.request.resources = []; + return Client.compile(this.project_id, this.request, (error, res, body) => { + this.error = error; + this.res = res; + this.body = body; + return done(); + }); + }); - it "should return a failure status", -> - @body.compile.status.should.equal "failure" + return it("should return a failure status", function() { + return this.body.compile.status.should.equal("failure"); + }); + }); + }); +}); diff --git a/test/acceptance/coffee/ExampleDocumentTests.js b/test/acceptance/coffee/ExampleDocumentTests.js index f8e4a777..fe899707 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.js +++ b/test/acceptance/coffee/ExampleDocumentTests.js @@ -1,129 +1,182 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -fs = require "fs" -ChildProcess = require "child_process" -ClsiApp = require "./helpers/ClsiApp" -logger = require("logger-sharelatex") -Path = require("path") -fixturePath = (path) -> Path.normalize(__dirname + "/../fixtures/" + path) -process = require "process" -console.log process.pid, process.ppid, process.getuid(),process.getgroups(), "PID" -try - console.log "creating tmp directory", fixturePath("tmp") - fs.mkdirSync(fixturePath("tmp")) -catch err - console.log err, fixturePath("tmp"), "unable to create fixture tmp path" - -MOCHA_LATEX_TIMEOUT = 60 * 1000 - -convertToPng = (pdfPath, pngPath, callback = (error) ->) -> - command = "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" - console.log "COMMAND" - console.log command - convert = ChildProcess.exec command - stdout = "" - convert.stdout.on "data", (chunk) -> console.log "STDOUT", chunk.toString() - convert.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() - convert.on "exit", () -> - callback() - -compare = (originalPath, generatedPath, callback = (error, same) ->) -> - diff_file = "#{fixturePath(generatedPath)}-diff.png" - proc = ChildProcess.exec "compare -metric mae #{fixturePath(originalPath)} #{fixturePath(generatedPath)} #{diff_file}" - stderr = "" - proc.stderr.on "data", (chunk) -> stderr += chunk - proc.on "exit", () -> - if stderr.trim() == "0 (0)" - # remove output diff if test matches expected image - fs.unlink diff_file, (err) -> - if err - throw err - callback null, true - else - console.log "compare result", stderr - callback null, false - -checkPdfInfo = (pdfPath, callback = (error, output) ->) -> - proc = ChildProcess.exec "pdfinfo #{fixturePath(pdfPath)}" - stdout = "" - proc.stdout.on "data", (chunk) -> stdout += chunk - proc.stderr.on "data", (chunk) -> console.log "STDERR", chunk.toString() - proc.on "exit", () -> - if stdout.match(/Optimized:\s+yes/) - callback null, true - else - callback null, false - -compareMultiplePages = (project_id, callback = (error) ->) -> - compareNext = (page_no, callback) -> - path = "tmp/#{project_id}-source-#{page_no}.png" - fs.stat fixturePath(path), (error, stat) -> - if error? - callback() - else - compare "tmp/#{project_id}-source-#{page_no}.png", "tmp/#{project_id}-generated-#{page_no}.png", (error, same) => - throw error if error? - same.should.equal true - compareNext page_no + 1, callback - compareNext 0, callback - -comparePdf = (project_id, example_dir, callback = (error) ->) -> - console.log "CONVERT" - console.log "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png" - convertToPng "tmp/#{project_id}.pdf", "tmp/#{project_id}-generated.png", (error) => - throw error if error? - convertToPng "examples/#{example_dir}/output.pdf", "tmp/#{project_id}-source.png", (error) => - throw error if error? - fs.stat fixturePath("tmp/#{project_id}-source-0.png"), (error, stat) => - if error? - compare "tmp/#{project_id}-source.png", "tmp/#{project_id}-generated.png", (error, same) => - throw error if error? - same.should.equal true - callback() - else - compareMultiplePages project_id, (error) -> - throw error if error? - callback() - -downloadAndComparePdf = (project_id, example_dir, url, callback = (error) ->) -> - writeStream = fs.createWriteStream(fixturePath("tmp/#{project_id}.pdf")) - request.get(url).pipe(writeStream) - console.log("writing file out", fixturePath("tmp/#{project_id}.pdf")) - writeStream.on "close", () => - checkPdfInfo "tmp/#{project_id}.pdf", (error, optimised) => - throw error if error? - optimised.should.equal true - comparePdf project_id, example_dir, callback - -Client.runServer(4242, fixturePath("examples")) - -describe "Example Documents", -> - before (done) -> - ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on "exit", () -> - ClsiApp.ensureRunning done - - - for example_dir in fs.readdirSync fixturePath("examples") - do (example_dir) -> - describe example_dir, -> - before -> - @project_id = Client.randomId() + "_" + example_dir - - it "should generate the correct pdf", (done) -> - this.timeout(MOCHA_LATEX_TIMEOUT) - Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - if error || body?.compile?.status is "failure" - console.log "DEBUG: error", error, "body", JSON.stringify(body) - pdf = Client.getOutputFile body, "pdf" - downloadAndComparePdf(@project_id, example_dir, pdf.url, done) - - it "should generate the correct pdf on the second run as well", (done) -> - this.timeout(MOCHA_LATEX_TIMEOUT) - Client.compileDirectory @project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => - if error || body?.compile?.status is "failure" - console.log "DEBUG: error", error, "body", JSON.stringify(body) - pdf = Client.getOutputFile body, "pdf" - downloadAndComparePdf(@project_id, example_dir, pdf.url, done) +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const fs = require("fs"); +const ChildProcess = require("child_process"); +const ClsiApp = require("./helpers/ClsiApp"); +const logger = require("logger-sharelatex"); +const Path = require("path"); +const fixturePath = path => Path.normalize(__dirname + "/../fixtures/" + path); +const process = require("process"); +console.log(process.pid, process.ppid, process.getuid(),process.getgroups(), "PID"); +try { + console.log("creating tmp directory", fixturePath("tmp")); + fs.mkdirSync(fixturePath("tmp")); +} catch (error) { + const err = error; + console.log(err, fixturePath("tmp"), "unable to create fixture tmp path"); +} +const MOCHA_LATEX_TIMEOUT = 60 * 1000; +const convertToPng = function(pdfPath, pngPath, callback) { + if (callback == null) { callback = function(error) {}; } + const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}`; + console.log("COMMAND"); + console.log(command); + const convert = ChildProcess.exec(command); + const stdout = ""; + convert.stdout.on("data", chunk => console.log("STDOUT", chunk.toString())); + convert.stderr.on("data", chunk => console.log("STDERR", chunk.toString())); + return convert.on("exit", () => callback()); +}; + +const compare = function(originalPath, generatedPath, callback) { + if (callback == null) { callback = function(error, same) {}; } + const diff_file = `${fixturePath(generatedPath)}-diff.png`; + const proc = ChildProcess.exec(`compare -metric mae ${fixturePath(originalPath)} ${fixturePath(generatedPath)} ${diff_file}`); + let stderr = ""; + proc.stderr.on("data", chunk => stderr += chunk); + return proc.on("exit", function() { + if (stderr.trim() === "0 (0)") { + // remove output diff if test matches expected image + fs.unlink(diff_file, function(err) { + if (err) { + throw err; + } + }); + return callback(null, true); + } else { + console.log("compare result", stderr); + return callback(null, false); + } + }); +}; + +const checkPdfInfo = function(pdfPath, callback) { + if (callback == null) { callback = function(error, output) {}; } + const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`); + let stdout = ""; + proc.stdout.on("data", chunk => stdout += chunk); + proc.stderr.on("data", chunk => console.log("STDERR", chunk.toString())); + return proc.on("exit", function() { + if (stdout.match(/Optimized:\s+yes/)) { + return callback(null, true); + } else { + return callback(null, false); + } + }); +}; + +const compareMultiplePages = function(project_id, callback) { + if (callback == null) { callback = function(error) {}; } + var compareNext = function(page_no, callback) { + const path = `tmp/${project_id}-source-${page_no}.png`; + return fs.stat(fixturePath(path), function(error, stat) { + if (error != null) { + return callback(); + } else { + return compare(`tmp/${project_id}-source-${page_no}.png`, `tmp/${project_id}-generated-${page_no}.png`, (error, same) => { + if (error != null) { throw error; } + same.should.equal(true); + return compareNext(page_no + 1, callback); + }); + } + }); + }; + return compareNext(0, callback); +}; + +const comparePdf = function(project_id, example_dir, callback) { + if (callback == null) { callback = function(error) {}; } + console.log("CONVERT"); + console.log(`tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`); + return convertToPng(`tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`, error => { + if (error != null) { throw error; } + return convertToPng(`examples/${example_dir}/output.pdf`, `tmp/${project_id}-source.png`, error => { + if (error != null) { throw error; } + return fs.stat(fixturePath(`tmp/${project_id}-source-0.png`), (error, stat) => { + if (error != null) { + return compare(`tmp/${project_id}-source.png`, `tmp/${project_id}-generated.png`, (error, same) => { + if (error != null) { throw error; } + same.should.equal(true); + return callback(); + }); + } else { + return compareMultiplePages(project_id, function(error) { + if (error != null) { throw error; } + return callback(); + }); + } + }); + }); + }); +}; + +const downloadAndComparePdf = function(project_id, example_dir, url, callback) { + if (callback == null) { callback = function(error) {}; } + const writeStream = fs.createWriteStream(fixturePath(`tmp/${project_id}.pdf`)); + request.get(url).pipe(writeStream); + console.log("writing file out", fixturePath(`tmp/${project_id}.pdf`)); + return writeStream.on("close", () => { + return checkPdfInfo(`tmp/${project_id}.pdf`, (error, optimised) => { + if (error != null) { throw error; } + optimised.should.equal(true); + return comparePdf(project_id, example_dir, callback); + }); + }); +}; + +Client.runServer(4242, fixturePath("examples")); + +describe("Example Documents", function() { + before(done => + ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on("exit", () => ClsiApp.ensureRunning(done)) + ); + + + return Array.from(fs.readdirSync(fixturePath("examples"))).map((example_dir) => + (example_dir => + describe(example_dir, function() { + before(function() { + return this.project_id = Client.randomId() + "_" + example_dir; + }); + + it("should generate the correct pdf", function(done) { + this.timeout(MOCHA_LATEX_TIMEOUT); + return Client.compileDirectory(this.project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => { + if (error || (__guard__(body != null ? body.compile : undefined, x => x.status) === "failure")) { + console.log("DEBUG: error", error, "body", JSON.stringify(body)); + } + const pdf = Client.getOutputFile(body, "pdf"); + return downloadAndComparePdf(this.project_id, example_dir, pdf.url, done); + }); + }); + + return it("should generate the correct pdf on the second run as well", function(done) { + this.timeout(MOCHA_LATEX_TIMEOUT); + return Client.compileDirectory(this.project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => { + if (error || (__guard__(body != null ? body.compile : undefined, x => x.status) === "failure")) { + console.log("DEBUG: error", error, "body", JSON.stringify(body)); + } + const pdf = Client.getOutputFile(body, "pdf"); + return downloadAndComparePdf(this.project_id, example_dir, pdf.url, done); + }); + }); + }) + )(example_dir)); +}); + + + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/test/acceptance/coffee/SimpleLatexFileTests.js b/test/acceptance/coffee/SimpleLatexFileTests.js index 95b667ba..79789e80 100644 --- a/test/acceptance/coffee/SimpleLatexFileTests.js +++ b/test/acceptance/coffee/SimpleLatexFileTests.js @@ -1,41 +1,57 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const ClsiApp = require("./helpers/ClsiApp"); -describe "Simple LaTeX file", -> - before (done) -> - @project_id = Client.randomId() - @request = - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' +describe("Simple LaTeX file", function() { + before(function(done) { + this.project_id = Client.randomId(); + this.request = { + resources: [{ + path: "main.tex", + content: `\ +\\documentclass{article} +\\begin{document} +Hello world +\\end{document}\ +` + } ] - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() + }; + return ClsiApp.ensureRunning(() => { + return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); + }); + }); - it "should return the PDF", -> - pdf = Client.getOutputFile(@body, "pdf") - pdf.type.should.equal "pdf" + it("should return the PDF", function() { + const pdf = Client.getOutputFile(this.body, "pdf"); + return pdf.type.should.equal("pdf"); + }); - it "should return the log", -> - log = Client.getOutputFile(@body, "log") - log.type.should.equal "log" + it("should return the log", function() { + const log = Client.getOutputFile(this.body, "log"); + return log.type.should.equal("log"); + }); - it "should provide the pdf for download", (done) -> - pdf = Client.getOutputFile(@body, "pdf") - request.get pdf.url, (error, res, body) -> - res.statusCode.should.equal 200 - done() + it("should provide the pdf for download", function(done) { + const pdf = Client.getOutputFile(this.body, "pdf"); + return request.get(pdf.url, function(error, res, body) { + res.statusCode.should.equal(200); + return done(); + }); + }); - it "should provide the log for download", (done) -> - log = Client.getOutputFile(@body, "pdf") - request.get log.url, (error, res, body) -> - res.statusCode.should.equal 200 - done() + return it("should provide the log for download", function(done) { + const log = Client.getOutputFile(this.body, "pdf"); + return request.get(log.url, function(error, res, body) { + res.statusCode.should.equal(200); + return done(); + }); + }); +}); diff --git a/test/acceptance/coffee/SynctexTests.js b/test/acceptance/coffee/SynctexTests.js index 685d2928..b0ac6881 100644 --- a/test/acceptance/coffee/SynctexTests.js +++ b/test/acceptance/coffee/SynctexTests.js @@ -1,41 +1,58 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -expect = require("chai").expect -ClsiApp = require "./helpers/ClsiApp" -crypto = require("crypto") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const { expect } = require("chai"); +const ClsiApp = require("./helpers/ClsiApp"); +const crypto = require("crypto"); -describe "Syncing", -> - before (done) -> - content = ''' - \\documentclass{article} - \\begin{document} - Hello world - \\end{document} - ''' - @request = - resources: [ - path: "main.tex" - content: content +describe("Syncing", function() { + before(function(done) { + const content = `\ +\\documentclass{article} +\\begin{document} +Hello world +\\end{document}\ +`; + this.request = { + resources: [{ + path: "main.tex", + content + } ] - @project_id = Client.randomId() - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() + }; + this.project_id = Client.randomId(); + return ClsiApp.ensureRunning(() => { + return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); + }); + }); - describe "from code to pdf", -> - it "should return the correct location", (done) -> - Client.syncFromCode @project_id, "main.tex", 3, 5, (error, pdfPositions) -> - throw error if error? - expect(pdfPositions).to.deep.equal( + describe("from code to pdf", () => + it("should return the correct location", function(done) { + return Client.syncFromCode(this.project_id, "main.tex", 3, 5, function(error, pdfPositions) { + if (error != null) { throw error; } + expect(pdfPositions).to.deep.equal({ pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] - ) - done() + }); + return done(); + }); + }) + ); - describe "from pdf to code", -> - it "should return the correct location", (done) -> - Client.syncFromPdf @project_id, 1, 100, 200, (error, codePositions) => - throw error if error? - expect(codePositions).to.deep.equal( + return describe("from pdf to code", () => + it("should return the correct location", function(done) { + return Client.syncFromPdf(this.project_id, 1, 100, 200, (error, codePositions) => { + if (error != null) { throw error; } + expect(codePositions).to.deep.equal({ code: [ { file: 'main.tex', line: 3, column: -1 } ] - ) - done() + }); + return done(); + }); + }) + ); +}); diff --git a/test/acceptance/coffee/TimeoutTests.js b/test/acceptance/coffee/TimeoutTests.js index b274dd54..39d18ed7 100644 --- a/test/acceptance/coffee/TimeoutTests.js +++ b/test/acceptance/coffee/TimeoutTests.js @@ -1,34 +1,48 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -ClsiApp = require "./helpers/ClsiApp" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const ClsiApp = require("./helpers/ClsiApp"); -describe "Timed out compile", -> - before (done) -> - @request = - options: - timeout: 10 #seconds - resources: [ - path: "main.tex" - content: ''' - \\documentclass{article} - \\begin{document} - \\def\\x{Hello!\\par\\x} - \\x - \\end{document} - ''' +describe("Timed out compile", function() { + before(function(done) { + this.request = { + options: { + timeout: 10 + }, //seconds + resources: [{ + path: "main.tex", + content: `\ +\\documentclass{article} +\\begin{document} +\\def\\x{Hello!\\par\\x} +\\x +\\end{document}\ +` + } ] - @project_id = Client.randomId() - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() + }; + this.project_id = Client.randomId(); + return ClsiApp.ensureRunning(() => { + return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); + }); + }); - it "should return a timeout error", -> - @body.compile.error.should.equal "container timed out" + it("should return a timeout error", function() { + return this.body.compile.error.should.equal("container timed out"); + }); - it "should return a timedout status", -> - @body.compile.status.should.equal "timedout" + it("should return a timedout status", function() { + return this.body.compile.status.should.equal("timedout"); + }); - it "should return the log output file name", -> - outputFilePaths = @body.compile.outputFiles.map((x) => x.path) - outputFilePaths.should.include('output.log') + return it("should return the log output file name", function() { + const outputFilePaths = this.body.compile.outputFiles.map(x => x.path); + return outputFilePaths.should.include('output.log'); + }); +}); diff --git a/test/acceptance/coffee/UrlCachingTests.js b/test/acceptance/coffee/UrlCachingTests.js index cef74467..3fe947ff 100644 --- a/test/acceptance/coffee/UrlCachingTests.js +++ b/test/acceptance/coffee/UrlCachingTests.js @@ -1,222 +1,280 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -sinon = require "sinon" -ClsiApp = require "./helpers/ClsiApp" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const sinon = require("sinon"); +const ClsiApp = require("./helpers/ClsiApp"); -host = "localhost" +const host = "localhost"; -Server = - run: () -> - express = require "express" - app = express() +const Server = { + run() { + const express = require("express"); + const app = express(); - staticServer = express.static __dirname + "/../fixtures/" - app.get "/:random_id/*", (req, res, next) => - @getFile(req.url) - req.url = "/" + req.params[0] - staticServer(req, res, next) + const staticServer = express.static(__dirname + "/../fixtures/"); + app.get("/:random_id/*", (req, res, next) => { + this.getFile(req.url); + req.url = `/${req.params[0]}`; + return staticServer(req, res, next); + }); - app.listen 31415, host + return app.listen(31415, host); + }, - getFile: () -> + getFile() {}, - randomId: () -> - Math.random().toString(16).slice(2) + randomId() { + return Math.random().toString(16).slice(2); + } +}; -Server.run() +Server.run(); -describe "Url Caching", -> - describe "Downloading an image for the first time", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = +describe("Url Caching", function() { + describe("Downloading an image for the first time", function() { + before(function(done) { + this.project_id = Client.randomId(); + this.file = `${Server.randomId()}/lion.png`; + this.request = { resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' + path: "main.tex", + content: `\ +\\documentclass{article} +\\usepackage{graphicx} +\\begin{document} +\\includegraphics{lion.png} +\\end{document}\ +` }, { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" + path: "lion.png", + url: `http://${host}:31415/${this.file}` }] + }; - sinon.spy Server, "getFile" - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() + sinon.spy(Server, "getFile"); + return ClsiApp.ensureRunning(() => { + return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); + }); + }); - afterEach -> - Server.getFile.restore() + afterEach(() => Server.getFile.restore()); - it "should download the image", -> - Server.getFile - .calledWith("/" + @file) - .should.equal true + return it("should download the image", function() { + return Server.getFile + .calledWith(`/${this.file}`) + .should.equal(true); + }); + }); - describe "When an image is in the cache and the last modified date is unchanged", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = + describe("When an image is in the cache and the last modified date is unchanged", function() { + before(function(done) { + this.project_id = Client.randomId(); + this.file = `${Server.randomId()}/lion.png`; + this.request = { resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" + path: "main.tex", + content: `\ +\\documentclass{article} +\\usepackage{graphicx} +\\begin{document} +\\includegraphics{lion.png} +\\end{document}\ +` + }, (this.image_resource = { + path: "lion.png", + url: `http://${host}:31415/${this.file}`, modified: Date.now() - }] + })] + }; - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - Client.compile @project_id, @request, (@error, @res, @body) => - done() + return Client.compile(this.project_id, this.request, (error, res, body) => { + this.error = error; + this.res = res; + this.body = body; + sinon.spy(Server, "getFile"); + return Client.compile(this.project_id, this.request, (error1, res1, body1) => { + this.error = error1; + this.res = res1; + this.body = body1; + return done(); + }); + }); + }); - after -> - Server.getFile.restore() + after(() => Server.getFile.restore()); - it "should not download the image again", -> - Server.getFile.called.should.equal false + return it("should not download the image again", () => Server.getFile.called.should.equal(false)); + }); - describe "When an image is in the cache and the last modified date is advanced", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = + describe("When an image is in the cache and the last modified date is advanced", function() { + before(function(done) { + this.project_id = Client.randomId(); + this.file = `${Server.randomId()}/lion.png`; + this.request = { resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] + path: "main.tex", + content: `\ +\\documentclass{article} +\\usepackage{graphicx} +\\begin{document} +\\includegraphics{lion.png} +\\end{document}\ +` + }, (this.image_resource = { + path: "lion.png", + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + })] + }; - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - @image_resource.modified = new Date(@last_modified + 3000) - Client.compile @project_id, @request, (@error, @res, @body) => - done() + return Client.compile(this.project_id, this.request, (error, res, body) => { + this.error = error; + this.res = res; + this.body = body; + sinon.spy(Server, "getFile"); + this.image_resource.modified = new Date(this.last_modified + 3000); + return Client.compile(this.project_id, this.request, (error1, res1, body1) => { + this.error = error1; + this.res = res1; + this.body = body1; + return done(); + }); + }); + }); - afterEach -> - Server.getFile.restore() + afterEach(() => Server.getFile.restore()); - it "should download the image again", -> - Server.getFile.called.should.equal true + return it("should download the image again", () => Server.getFile.called.should.equal(true)); + }); - describe "When an image is in the cache and the last modified date is further in the past", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = + describe("When an image is in the cache and the last modified date is further in the past", function() { + before(function(done) { + this.project_id = Client.randomId(); + this.file = `${Server.randomId()}/lion.png`; + this.request = { resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] + path: "main.tex", + content: `\ +\\documentclass{article} +\\usepackage{graphicx} +\\begin{document} +\\includegraphics{lion.png} +\\end{document}\ +` + }, (this.image_resource = { + path: "lion.png", + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + })] + }; - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - @image_resource.modified = new Date(@last_modified - 3000) - Client.compile @project_id, @request, (@error, @res, @body) => - done() + return Client.compile(this.project_id, this.request, (error, res, body) => { + this.error = error; + this.res = res; + this.body = body; + sinon.spy(Server, "getFile"); + this.image_resource.modified = new Date(this.last_modified - 3000); + return Client.compile(this.project_id, this.request, (error1, res1, body1) => { + this.error = error1; + this.res = res1; + this.body = body1; + return done(); + }); + }); + }); - afterEach -> - Server.getFile.restore() + afterEach(() => Server.getFile.restore()); - it "should not download the image again", -> - Server.getFile.called.should.equal false + return it("should not download the image again", () => Server.getFile.called.should.equal(false)); + }); - describe "When an image is in the cache and the last modified date is not specified", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = + describe("When an image is in the cache and the last modified date is not specified", function() { + before(function(done) { + this.project_id = Client.randomId(); + this.file = `${Server.randomId()}/lion.png`; + this.request = { resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] + path: "main.tex", + content: `\ +\\documentclass{article} +\\usepackage{graphicx} +\\begin{document} +\\includegraphics{lion.png} +\\end{document}\ +` + }, (this.image_resource = { + path: "lion.png", + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + })] + }; - Client.compile @project_id, @request, (@error, @res, @body) => - sinon.spy Server, "getFile" - delete @image_resource.modified - Client.compile @project_id, @request, (@error, @res, @body) => - done() + return Client.compile(this.project_id, this.request, (error, res, body) => { + this.error = error; + this.res = res; + this.body = body; + sinon.spy(Server, "getFile"); + delete this.image_resource.modified; + return Client.compile(this.project_id, this.request, (error1, res1, body1) => { + this.error = error1; + this.res = res1; + this.body = body1; + return done(); + }); + }); + }); - afterEach -> - Server.getFile.restore() + afterEach(() => Server.getFile.restore()); - it "should download the image again", -> - Server.getFile.called.should.equal true + return it("should download the image again", () => Server.getFile.called.should.equal(true)); + }); - describe "After clearing the cache", -> - before (done) -> - @project_id = Client.randomId() - @file = "#{Server.randomId()}/lion.png" - @request = + return describe("After clearing the cache", function() { + before(function(done) { + this.project_id = Client.randomId(); + this.file = `${Server.randomId()}/lion.png`; + this.request = { resources: [{ - path: "main.tex" - content: ''' - \\documentclass{article} - \\usepackage{graphicx} - \\begin{document} - \\includegraphics{lion.png} - \\end{document} - ''' - }, @image_resource = { - path: "lion.png" - url: "http://#{host}:31415/#{@file}" - modified: @last_modified = Date.now() - }] + path: "main.tex", + content: `\ +\\documentclass{article} +\\usepackage{graphicx} +\\begin{document} +\\includegraphics{lion.png} +\\end{document}\ +` + }, (this.image_resource = { + path: "lion.png", + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + })] + }; - Client.compile @project_id, @request, (error) => - throw error if error? - Client.clearCache @project_id, (error, res, body) => - throw error if error? - sinon.spy Server, "getFile" - Client.compile @project_id, @request, (@error, @res, @body) => - done() + return Client.compile(this.project_id, this.request, error => { + if (error != null) { throw error; } + return Client.clearCache(this.project_id, (error, res, body) => { + if (error != null) { throw error; } + sinon.spy(Server, "getFile"); + return Client.compile(this.project_id, this.request, (error1, res1, body1) => { + this.error = error1; + this.res = res1; + this.body = body1; + return done(); + }); + }); + }); + }); - afterEach -> - Server.getFile.restore() + afterEach(() => Server.getFile.restore()); - it "should download the image again", -> - Server.getFile.called.should.equal true + return it("should download the image again", () => Server.getFile.called.should.equal(true)); + }); +}); diff --git a/test/acceptance/coffee/WordcountTests.js b/test/acceptance/coffee/WordcountTests.js index abace066..8c87a7cd 100644 --- a/test/acceptance/coffee/WordcountTests.js +++ b/test/acceptance/coffee/WordcountTests.js @@ -1,38 +1,52 @@ -Client = require "./helpers/Client" -request = require "request" -require("chai").should() -expect = require("chai").expect -path = require("path") -fs = require("fs") -ClsiApp = require "./helpers/ClsiApp" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Client = require("./helpers/Client"); +const request = require("request"); +require("chai").should(); +const { expect } = require("chai"); +const path = require("path"); +const fs = require("fs"); +const ClsiApp = require("./helpers/ClsiApp"); -describe "Syncing", -> - before (done) -> - @request = - resources: [ - path: "main.tex" +describe("Syncing", function() { + before(function(done) { + this.request = { + resources: [{ + path: "main.tex", content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") + } ] - @project_id = Client.randomId() - ClsiApp.ensureRunning => - Client.compile @project_id, @request, (@error, @res, @body) => done() + }; + this.project_id = Client.randomId(); + return ClsiApp.ensureRunning(() => { + return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); + }); + }); - describe "wordcount file", -> - it "should return wordcount info", (done) -> - Client.wordcount @project_id, "main.tex", (error, result) -> - throw error if error? - expect(result).to.deep.equal( + return describe("wordcount file", () => + it("should return wordcount info", function(done) { + return Client.wordcount(this.project_id, "main.tex", function(error, result) { + if (error != null) { throw error; } + expect(result).to.deep.equal({ texcount: { - encode: "utf8" - textWords: 2281 - headWords: 2 - outside: 0 - headers: 2 - elements: 0 - mathInline: 6 - mathDisplay: 0 - errors: 0 + encode: "utf8", + textWords: 2281, + headWords: 2, + outside: 0, + headers: 2, + elements: 0, + mathInline: 6, + mathDisplay: 0, + errors: 0, messages: "" } - ) - done() + }); + return done(); + }); + }) + ); +}); diff --git a/test/acceptance/coffee/helpers/Client.js b/test/acceptance/coffee/helpers/Client.js index 39131703..4b85413a 100644 --- a/test/acceptance/coffee/helpers/Client.js +++ b/test/acceptance/coffee/helpers/Client.js @@ -1,105 +1,147 @@ -request = require "request" -fs = require "fs" -Settings = require "settings-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Client; +const request = require("request"); +const fs = require("fs"); +const Settings = require("settings-sharelatex"); -host = "localhost" +const host = "localhost"; -module.exports = Client = - host: Settings.apis.clsi.url +module.exports = (Client = { + host: Settings.apis.clsi.url, - randomId: () -> - Math.random().toString(16).slice(2) + randomId() { + return Math.random().toString(16).slice(2); + }, - compile: (project_id, data, callback = (error, res, body) ->) -> - request.post { - url: "#{@host}/project/#{project_id}/compile" - json: + compile(project_id, data, callback) { + if (callback == null) { callback = function(error, res, body) {}; } + return request.post({ + url: `${this.host}/project/${project_id}/compile`, + json: { compile: data - }, callback + } + }, callback); + }, - clearCache: (project_id, callback = (error, res, body) ->) -> - request.del "#{@host}/project/#{project_id}", callback + clearCache(project_id, callback) { + if (callback == null) { callback = function(error, res, body) {}; } + return request.del(`${this.host}/project/${project_id}`, callback); + }, - getOutputFile: (response, type) -> - for file in response.compile.outputFiles - if file.type == type and file.url.match("output.#{type}") - return file - return null + getOutputFile(response, type) { + for (let file of Array.from(response.compile.outputFiles)) { + if ((file.type === type) && file.url.match(`output.${type}`)) { + return file; + } + } + return null; + }, - runServer: (port, directory) -> - express = require("express") - app = express() - app.use express.static(directory) - console.log("starting test server on", port, host) - app.listen(port, host).on "error", (error) -> - console.error "error starting server:", error.message - process.exit(1) + runServer(port, directory) { + const express = require("express"); + const app = express(); + app.use(express.static(directory)); + console.log("starting test server on", port, host); + return app.listen(port, host).on("error", function(error) { + console.error("error starting server:", error.message); + return process.exit(1); + }); + }, - syncFromCode: (project_id, file, line, column, callback = (error, pdfPositions) ->) -> - request.get { - url: "#{@host}/project/#{project_id}/sync/code" + syncFromCode(project_id, file, line, column, callback) { + if (callback == null) { callback = function(error, pdfPositions) {}; } + return request.get({ + url: `${this.host}/project/${project_id}/sync/code`, qs: { - file: file - line: line - column: column + file, + line, + column } - }, (error, response, body) -> - return callback(error) if error? - callback null, JSON.parse(body) + }, function(error, response, body) { + if (error != null) { return callback(error); } + return callback(null, JSON.parse(body)); + }); + }, - syncFromPdf: (project_id, page, h, v, callback = (error, pdfPositions) ->) -> - request.get { - url: "#{@host}/project/#{project_id}/sync/pdf" + syncFromPdf(project_id, page, h, v, callback) { + if (callback == null) { callback = function(error, pdfPositions) {}; } + return request.get({ + url: `${this.host}/project/${project_id}/sync/pdf`, qs: { - page: page, - h: h, v: v + page, + h, v } - }, (error, response, body) -> - return callback(error) if error? - callback null, JSON.parse(body) + }, function(error, response, body) { + if (error != null) { return callback(error); } + return callback(null, JSON.parse(body)); + }); + }, - compileDirectory: (project_id, baseDirectory, directory, serverPort, callback = (error, res, body) ->) -> - resources = [] - entities = fs.readdirSync("#{baseDirectory}/#{directory}") - rootResourcePath = "main.tex" - while (entities.length > 0) - entity = entities.pop() - stat = fs.statSync("#{baseDirectory}/#{directory}/#{entity}") - if stat.isDirectory() - entities = entities.concat fs.readdirSync("#{baseDirectory}/#{directory}/#{entity}").map (subEntity) -> - if subEntity == "main.tex" - rootResourcePath = "#{entity}/#{subEntity}" - return "#{entity}/#{subEntity}" - else if stat.isFile() and entity != "output.pdf" - extension = entity.split(".").pop() - if ["tex", "bib", "cls", "sty", "pdf_tex", "Rtex", "ist", "md", "Rmd"].indexOf(extension) > -1 - resources.push - path: entity - content: fs.readFileSync("#{baseDirectory}/#{directory}/#{entity}").toString() - else if ["eps", "ttf", "png", "jpg", "pdf", "jpeg"].indexOf(extension) > -1 - resources.push - path: entity - url: "http://#{host}:#{serverPort}/#{directory}/#{entity}" + compileDirectory(project_id, baseDirectory, directory, serverPort, callback) { + if (callback == null) { callback = function(error, res, body) {}; } + const resources = []; + let entities = fs.readdirSync(`${baseDirectory}/${directory}`); + let rootResourcePath = "main.tex"; + while (entities.length > 0) { + var entity = entities.pop(); + const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`); + if (stat.isDirectory()) { + entities = entities.concat(fs.readdirSync(`${baseDirectory}/${directory}/${entity}`).map(function(subEntity) { + if (subEntity === "main.tex") { + rootResourcePath = `${entity}/${subEntity}`; + } + return `${entity}/${subEntity}`; + }) + ); + } else if (stat.isFile() && (entity !== "output.pdf")) { + const extension = entity.split(".").pop(); + if (["tex", "bib", "cls", "sty", "pdf_tex", "Rtex", "ist", "md", "Rmd"].indexOf(extension) > -1) { + resources.push({ + path: entity, + content: fs.readFileSync(`${baseDirectory}/${directory}/${entity}`).toString() + }); + } else if (["eps", "ttf", "png", "jpg", "pdf", "jpeg"].indexOf(extension) > -1) { + resources.push({ + path: entity, + url: `http://${host}:${serverPort}/${directory}/${entity}`, modified: stat.mtime + }); + } + } + } - fs.readFile "#{baseDirectory}/#{directory}/options.json", (error, body) => - req = - resources: resources - rootResourcePath: rootResourcePath + return fs.readFile(`${baseDirectory}/${directory}/options.json`, (error, body) => { + const req = { + resources, + rootResourcePath + }; - if !error? - body = JSON.parse body - req.options = body + if ((error == null)) { + body = JSON.parse(body); + req.options = body; + } - @compile project_id, req, callback + return this.compile(project_id, req, callback); + }); + }, - wordcount: (project_id, file, callback = (error, pdfPositions) ->) -> - request.get { - url: "#{@host}/project/#{project_id}/wordcount" + wordcount(project_id, file, callback) { + if (callback == null) { callback = function(error, pdfPositions) {}; } + return request.get({ + url: `${this.host}/project/${project_id}/wordcount`, qs: { - file: file + file } - }, (error, response, body) -> - return callback(error) if error? - callback null, JSON.parse(body) + }, function(error, response, body) { + if (error != null) { return callback(error); } + return callback(null, JSON.parse(body)); + }); + } +}); diff --git a/test/acceptance/coffee/helpers/ClsiApp.js b/test/acceptance/coffee/helpers/ClsiApp.js index d9cd534b..cad63ecd 100644 --- a/test/acceptance/coffee/helpers/ClsiApp.js +++ b/test/acceptance/coffee/helpers/ClsiApp.js @@ -1,24 +1,46 @@ -app = require('../../../../app') -require("logger-sharelatex").logger.level("info") -logger = require("logger-sharelatex") -Settings = require("settings-sharelatex") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const app = require('../../../../app'); +require("logger-sharelatex").logger.level("info"); +const logger = require("logger-sharelatex"); +const Settings = require("settings-sharelatex"); -module.exports = - running: false - initing: false - callbacks: [] - ensureRunning: (callback = (error) ->) -> - if @running - return callback() - else if @initing - @callbacks.push callback - else - @initing = true - @callbacks.push callback - app.listen Settings.internal?.clsi?.port, "localhost", (error) => - throw error if error? - @running = true - logger.log("clsi running in dev mode") +module.exports = { + running: false, + initing: false, + callbacks: [], + ensureRunning(callback) { + if (callback == null) { callback = function(error) {}; } + if (this.running) { + return callback(); + } else if (this.initing) { + return this.callbacks.push(callback); + } else { + this.initing = true; + this.callbacks.push(callback); + return app.listen(__guard__(Settings.internal != null ? Settings.internal.clsi : undefined, x => x.port), "localhost", error => { + if (error != null) { throw error; } + this.running = true; + logger.log("clsi running in dev mode"); - for callback in @callbacks - callback() \ No newline at end of file + return (() => { + const result = []; + for (callback of Array.from(this.callbacks)) { + result.push(callback()); + } + return result; + })(); + }); + } + } +}; +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file From 95854a3abbb6a56132e692a08c975f222a3e1ae3 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:16:07 +0100 Subject: [PATCH 482/709] decaffeinate: Run post-processing cleanups on BrokenLatexFileTests.coffee and 9 other files --- .../acceptance/coffee/BrokenLatexFileTests.js | 5 ++++ test/acceptance/coffee/DeleteOldFilesTest.js | 5 ++++ .../acceptance/coffee/ExampleDocumentTests.js | 22 ++++++++++----- .../acceptance/coffee/SimpleLatexFileTests.js | 9 ++++-- test/acceptance/coffee/SynctexTests.js | 17 ++++++----- test/acceptance/coffee/TimeoutTests.js | 7 ++++- test/acceptance/coffee/UrlCachingTests.js | 28 +++++++++++-------- test/acceptance/coffee/WordcountTests.js | 12 +++++--- test/acceptance/coffee/helpers/Client.js | 19 +++++++++---- test/acceptance/coffee/helpers/ClsiApp.js | 5 ++++ 10 files changed, 91 insertions(+), 38 deletions(-) diff --git a/test/acceptance/coffee/BrokenLatexFileTests.js b/test/acceptance/coffee/BrokenLatexFileTests.js index 5aea6257..2db36c14 100644 --- a/test/acceptance/coffee/BrokenLatexFileTests.js +++ b/test/acceptance/coffee/BrokenLatexFileTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/acceptance/coffee/DeleteOldFilesTest.js b/test/acceptance/coffee/DeleteOldFilesTest.js index d6958c20..720b90f2 100644 --- a/test/acceptance/coffee/DeleteOldFilesTest.js +++ b/test/acceptance/coffee/DeleteOldFilesTest.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/test/acceptance/coffee/ExampleDocumentTests.js b/test/acceptance/coffee/ExampleDocumentTests.js index fe899707..4c3080f3 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.js +++ b/test/acceptance/coffee/ExampleDocumentTests.js @@ -1,3 +1,12 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-path-concat, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -45,10 +54,10 @@ const compare = function(originalPath, generatedPath, callback) { const proc = ChildProcess.exec(`compare -metric mae ${fixturePath(originalPath)} ${fixturePath(generatedPath)} ${diff_file}`); let stderr = ""; proc.stderr.on("data", chunk => stderr += chunk); - return proc.on("exit", function() { + return proc.on("exit", () => { if (stderr.trim() === "0 (0)") { // remove output diff if test matches expected image - fs.unlink(diff_file, function(err) { + fs.unlink(diff_file, (err) => { if (err) { throw err; } @@ -67,7 +76,7 @@ const checkPdfInfo = function(pdfPath, callback) { let stdout = ""; proc.stdout.on("data", chunk => stdout += chunk); proc.stderr.on("data", chunk => console.log("STDERR", chunk.toString())); - return proc.on("exit", function() { + return proc.on("exit", () => { if (stdout.match(/Optimized:\s+yes/)) { return callback(null, true); } else { @@ -80,7 +89,7 @@ const compareMultiplePages = function(project_id, callback) { if (callback == null) { callback = function(error) {}; } var compareNext = function(page_no, callback) { const path = `tmp/${project_id}-source-${page_no}.png`; - return fs.stat(fixturePath(path), function(error, stat) { + return fs.stat(fixturePath(path), (error, stat) => { if (error != null) { return callback(); } else { @@ -111,7 +120,7 @@ const comparePdf = function(project_id, example_dir, callback) { return callback(); }); } else { - return compareMultiplePages(project_id, function(error) { + return compareMultiplePages(project_id, (error) => { if (error != null) { throw error; } return callback(); }); @@ -138,8 +147,7 @@ const downloadAndComparePdf = function(project_id, example_dir, url, callback) { Client.runServer(4242, fixturePath("examples")); describe("Example Documents", function() { - before(done => - ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on("exit", () => ClsiApp.ensureRunning(done)) + before(function(done) { return ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on("exit", () => ClsiApp.ensureRunning(done)); } ); diff --git a/test/acceptance/coffee/SimpleLatexFileTests.js b/test/acceptance/coffee/SimpleLatexFileTests.js index 79789e80..d774301d 100644 --- a/test/acceptance/coffee/SimpleLatexFileTests.js +++ b/test/acceptance/coffee/SimpleLatexFileTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -40,7 +45,7 @@ Hello world it("should provide the pdf for download", function(done) { const pdf = Client.getOutputFile(this.body, "pdf"); - return request.get(pdf.url, function(error, res, body) { + return request.get(pdf.url, (error, res, body) => { res.statusCode.should.equal(200); return done(); }); @@ -48,7 +53,7 @@ Hello world return it("should provide the log for download", function(done) { const log = Client.getOutputFile(this.body, "pdf"); - return request.get(log.url, function(error, res, body) { + return request.get(log.url, (error, res, body) => { res.statusCode.should.equal(200); return done(); }); diff --git a/test/acceptance/coffee/SynctexTests.js b/test/acceptance/coffee/SynctexTests.js index b0ac6881..d8879ebc 100644 --- a/test/acceptance/coffee/SynctexTests.js +++ b/test/acceptance/coffee/SynctexTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -32,20 +37,18 @@ Hello world }); }); - describe("from code to pdf", () => - it("should return the correct location", function(done) { - return Client.syncFromCode(this.project_id, "main.tex", 3, 5, function(error, pdfPositions) { + describe("from code to pdf", function() { return it("should return the correct location", function(done) { + return Client.syncFromCode(this.project_id, "main.tex", 3, 5, (error, pdfPositions) => { if (error != null) { throw error; } expect(pdfPositions).to.deep.equal({ pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] }); return done(); }); - }) + }); } ); - return describe("from pdf to code", () => - it("should return the correct location", function(done) { + return describe("from pdf to code", function() { return it("should return the correct location", function(done) { return Client.syncFromPdf(this.project_id, 1, 100, 200, (error, codePositions) => { if (error != null) { throw error; } expect(codePositions).to.deep.equal({ @@ -53,6 +56,6 @@ Hello world }); return done(); }); - }) + }); } ); }); diff --git a/test/acceptance/coffee/TimeoutTests.js b/test/acceptance/coffee/TimeoutTests.js index 39d18ed7..7f8f8483 100644 --- a/test/acceptance/coffee/TimeoutTests.js +++ b/test/acceptance/coffee/TimeoutTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -14,7 +19,7 @@ describe("Timed out compile", function() { this.request = { options: { timeout: 10 - }, //seconds + }, // seconds resources: [{ path: "main.tex", content: `\ diff --git a/test/acceptance/coffee/UrlCachingTests.js b/test/acceptance/coffee/UrlCachingTests.js index 3fe947ff..7bb0a205 100644 --- a/test/acceptance/coffee/UrlCachingTests.js +++ b/test/acceptance/coffee/UrlCachingTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-path-concat, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -63,7 +69,7 @@ describe("Url Caching", function() { }); }); - afterEach(() => Server.getFile.restore()); + afterEach(function() { return Server.getFile.restore(); }); return it("should download the image", function() { return Server.getFile @@ -107,9 +113,9 @@ describe("Url Caching", function() { }); }); - after(() => Server.getFile.restore()); + after(function() { return Server.getFile.restore(); }); - return it("should not download the image again", () => Server.getFile.called.should.equal(false)); + return it("should not download the image again", function() { return Server.getFile.called.should.equal(false); }); }); describe("When an image is in the cache and the last modified date is advanced", function() { @@ -148,9 +154,9 @@ describe("Url Caching", function() { }); }); - afterEach(() => Server.getFile.restore()); + afterEach(function() { return Server.getFile.restore(); }); - return it("should download the image again", () => Server.getFile.called.should.equal(true)); + return it("should download the image again", function() { return Server.getFile.called.should.equal(true); }); }); describe("When an image is in the cache and the last modified date is further in the past", function() { @@ -189,9 +195,9 @@ describe("Url Caching", function() { }); }); - afterEach(() => Server.getFile.restore()); + afterEach(function() { return Server.getFile.restore(); }); - return it("should not download the image again", () => Server.getFile.called.should.equal(false)); + return it("should not download the image again", function() { return Server.getFile.called.should.equal(false); }); }); describe("When an image is in the cache and the last modified date is not specified", function() { @@ -230,9 +236,9 @@ describe("Url Caching", function() { }); }); - afterEach(() => Server.getFile.restore()); + afterEach(function() { return Server.getFile.restore(); }); - return it("should download the image again", () => Server.getFile.called.should.equal(true)); + return it("should download the image again", function() { return Server.getFile.called.should.equal(true); }); }); return describe("After clearing the cache", function() { @@ -271,9 +277,9 @@ describe("Url Caching", function() { }); }); - afterEach(() => Server.getFile.restore()); + afterEach(function() { return Server.getFile.restore(); }); - return it("should download the image again", () => Server.getFile.called.should.equal(true)); + return it("should download the image again", function() { return Server.getFile.called.should.equal(true); }); }); }); diff --git a/test/acceptance/coffee/WordcountTests.js b/test/acceptance/coffee/WordcountTests.js index 8c87a7cd..2f81e139 100644 --- a/test/acceptance/coffee/WordcountTests.js +++ b/test/acceptance/coffee/WordcountTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -27,9 +32,8 @@ describe("Syncing", function() { }); }); - return describe("wordcount file", () => - it("should return wordcount info", function(done) { - return Client.wordcount(this.project_id, "main.tex", function(error, result) { + return describe("wordcount file", function() { return it("should return wordcount info", function(done) { + return Client.wordcount(this.project_id, "main.tex", (error, result) => { if (error != null) { throw error; } expect(result).to.deep.equal({ texcount: { @@ -47,6 +51,6 @@ describe("Syncing", function() { }); return done(); }); - }) + }); } ); }); diff --git a/test/acceptance/coffee/helpers/Client.js b/test/acceptance/coffee/helpers/Client.js index 4b85413a..50e75d61 100644 --- a/test/acceptance/coffee/helpers/Client.js +++ b/test/acceptance/coffee/helpers/Client.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -35,7 +42,7 @@ module.exports = (Client = { }, getOutputFile(response, type) { - for (let file of Array.from(response.compile.outputFiles)) { + for (const file of Array.from(response.compile.outputFiles)) { if ((file.type === type) && file.url.match(`output.${type}`)) { return file; } @@ -48,7 +55,7 @@ module.exports = (Client = { const app = express(); app.use(express.static(directory)); console.log("starting test server on", port, host); - return app.listen(port, host).on("error", function(error) { + return app.listen(port, host).on("error", (error) => { console.error("error starting server:", error.message); return process.exit(1); }); @@ -64,7 +71,7 @@ module.exports = (Client = { line, column } - }, function(error, response, body) { + }, (error, response, body) => { if (error != null) { return callback(error); } return callback(null, JSON.parse(body)); }); @@ -78,7 +85,7 @@ module.exports = (Client = { page, h, v } - }, function(error, response, body) { + }, (error, response, body) => { if (error != null) { return callback(error); } return callback(null, JSON.parse(body)); }); @@ -93,7 +100,7 @@ module.exports = (Client = { var entity = entities.pop(); const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`); if (stat.isDirectory()) { - entities = entities.concat(fs.readdirSync(`${baseDirectory}/${directory}/${entity}`).map(function(subEntity) { + entities = entities.concat(fs.readdirSync(`${baseDirectory}/${directory}/${entity}`).map((subEntity) => { if (subEntity === "main.tex") { rootResourcePath = `${entity}/${subEntity}`; } @@ -139,7 +146,7 @@ module.exports = (Client = { qs: { file } - }, function(error, response, body) { + }, (error, response, body) => { if (error != null) { return callback(error); } return callback(null, JSON.parse(body)); }); diff --git a/test/acceptance/coffee/helpers/ClsiApp.js b/test/acceptance/coffee/helpers/ClsiApp.js index cad63ecd..bd3222df 100644 --- a/test/acceptance/coffee/helpers/ClsiApp.js +++ b/test/acceptance/coffee/helpers/ClsiApp.js @@ -1,3 +1,8 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from From 7996f4494247302c1eb5ff459e5ba9e50637fe2a Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:16:11 +0100 Subject: [PATCH 483/709] decaffeinate: rename test/acceptance/coffee to test/acceptance/js --- test/acceptance/{coffee => js}/BrokenLatexFileTests.js | 0 test/acceptance/{coffee => js}/DeleteOldFilesTest.js | 0 test/acceptance/{coffee => js}/ExampleDocumentTests.js | 0 test/acceptance/{coffee => js}/SimpleLatexFileTests.js | 0 test/acceptance/{coffee => js}/SynctexTests.js | 0 test/acceptance/{coffee => js}/TimeoutTests.js | 0 test/acceptance/{coffee => js}/UrlCachingTests.js | 0 test/acceptance/{coffee => js}/WordcountTests.js | 0 test/acceptance/{coffee => js}/helpers/Client.js | 0 test/acceptance/{coffee => js}/helpers/ClsiApp.js | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename test/acceptance/{coffee => js}/BrokenLatexFileTests.js (100%) rename test/acceptance/{coffee => js}/DeleteOldFilesTest.js (100%) rename test/acceptance/{coffee => js}/ExampleDocumentTests.js (100%) rename test/acceptance/{coffee => js}/SimpleLatexFileTests.js (100%) rename test/acceptance/{coffee => js}/SynctexTests.js (100%) rename test/acceptance/{coffee => js}/TimeoutTests.js (100%) rename test/acceptance/{coffee => js}/UrlCachingTests.js (100%) rename test/acceptance/{coffee => js}/WordcountTests.js (100%) rename test/acceptance/{coffee => js}/helpers/Client.js (100%) rename test/acceptance/{coffee => js}/helpers/ClsiApp.js (100%) diff --git a/test/acceptance/coffee/BrokenLatexFileTests.js b/test/acceptance/js/BrokenLatexFileTests.js similarity index 100% rename from test/acceptance/coffee/BrokenLatexFileTests.js rename to test/acceptance/js/BrokenLatexFileTests.js diff --git a/test/acceptance/coffee/DeleteOldFilesTest.js b/test/acceptance/js/DeleteOldFilesTest.js similarity index 100% rename from test/acceptance/coffee/DeleteOldFilesTest.js rename to test/acceptance/js/DeleteOldFilesTest.js diff --git a/test/acceptance/coffee/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js similarity index 100% rename from test/acceptance/coffee/ExampleDocumentTests.js rename to test/acceptance/js/ExampleDocumentTests.js diff --git a/test/acceptance/coffee/SimpleLatexFileTests.js b/test/acceptance/js/SimpleLatexFileTests.js similarity index 100% rename from test/acceptance/coffee/SimpleLatexFileTests.js rename to test/acceptance/js/SimpleLatexFileTests.js diff --git a/test/acceptance/coffee/SynctexTests.js b/test/acceptance/js/SynctexTests.js similarity index 100% rename from test/acceptance/coffee/SynctexTests.js rename to test/acceptance/js/SynctexTests.js diff --git a/test/acceptance/coffee/TimeoutTests.js b/test/acceptance/js/TimeoutTests.js similarity index 100% rename from test/acceptance/coffee/TimeoutTests.js rename to test/acceptance/js/TimeoutTests.js diff --git a/test/acceptance/coffee/UrlCachingTests.js b/test/acceptance/js/UrlCachingTests.js similarity index 100% rename from test/acceptance/coffee/UrlCachingTests.js rename to test/acceptance/js/UrlCachingTests.js diff --git a/test/acceptance/coffee/WordcountTests.js b/test/acceptance/js/WordcountTests.js similarity index 100% rename from test/acceptance/coffee/WordcountTests.js rename to test/acceptance/js/WordcountTests.js diff --git a/test/acceptance/coffee/helpers/Client.js b/test/acceptance/js/helpers/Client.js similarity index 100% rename from test/acceptance/coffee/helpers/Client.js rename to test/acceptance/js/helpers/Client.js diff --git a/test/acceptance/coffee/helpers/ClsiApp.js b/test/acceptance/js/helpers/ClsiApp.js similarity index 100% rename from test/acceptance/coffee/helpers/ClsiApp.js rename to test/acceptance/js/helpers/ClsiApp.js From 71a50dd11fb9f235e48c9a9d5f14b2a3c88ce177 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:16:14 +0100 Subject: [PATCH 484/709] prettier: convert test/acceptance decaffeinated files to Prettier format --- test/acceptance/js/BrokenLatexFileTests.js | 115 ++--- test/acceptance/js/DeleteOldFilesTest.js | 93 ++-- test/acceptance/js/ExampleDocumentTests.js | 390 ++++++++++------ test/acceptance/js/SimpleLatexFileTests.js | 97 ++-- test/acceptance/js/SynctexTests.js | 116 +++-- test/acceptance/js/TimeoutTests.js | 75 +-- test/acceptance/js/UrlCachingTests.js | 503 ++++++++++++--------- test/acceptance/js/WordcountTests.js | 102 +++-- test/acceptance/js/helpers/Client.js | 306 +++++++------ test/acceptance/js/helpers/ClsiApp.js | 79 ++-- 10 files changed, 1105 insertions(+), 771 deletions(-) diff --git a/test/acceptance/js/BrokenLatexFileTests.js b/test/acceptance/js/BrokenLatexFileTests.js index 2db36c14..b34d23ca 100644 --- a/test/acceptance/js/BrokenLatexFileTests.js +++ b/test/acceptance/js/BrokenLatexFileTests.js @@ -8,68 +8,81 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const ClsiApp = require("./helpers/ClsiApp"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const ClsiApp = require('./helpers/ClsiApp') -describe("Broken LaTeX file", function() { - before(function(done){ - this.broken_request = { - resources: [{ - path: "main.tex", - content: `\ +describe('Broken LaTeX file', function() { + before(function(done) { + this.broken_request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{articl % :( \\begin{documen % :( Broken \\end{documen % :(\ ` - } - ] - }; - this.correct_request = { - resources: [{ - path: "main.tex", - content: `\ + } + ] + } + this.correct_request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\begin{document} Hello world \\end{document}\ ` - } - ] - }; - return ClsiApp.ensureRunning(done); - }); - - describe("on first run", function() { - before(function(done) { - this.project_id = Client.randomId(); - return Client.compile(this.project_id, this.broken_request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); - }); + } + ] + } + return ClsiApp.ensureRunning(done) + }) - return it("should return a failure status", function() { - return this.body.compile.status.should.equal("failure"); - }); - }); + describe('on first run', function() { + before(function(done) { + this.project_id = Client.randomId() + return Client.compile( + this.project_id, + this.broken_request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) - return describe("on second run", function() { - before(function(done) { - this.project_id = Client.randomId(); - return Client.compile(this.project_id, this.correct_request, () => { - return Client.compile(this.project_id, this.broken_request, (error, res, body) => { - this.error = error; - this.res = res; - this.body = body; - return done(); - }); - }); - }); + return it('should return a failure status', function() { + return this.body.compile.status.should.equal('failure') + }) + }) - return it("should return a failure status", function() { - return this.body.compile.status.should.equal("failure"); - }); - }); -}); - - + return describe('on second run', function() { + before(function(done) { + this.project_id = Client.randomId() + return Client.compile(this.project_id, this.correct_request, () => { + return Client.compile( + this.project_id, + this.broken_request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + }) + + return it('should return a failure status', function() { + return this.body.compile.status.should.equal('failure') + }) + }) +}) diff --git a/test/acceptance/js/DeleteOldFilesTest.js b/test/acceptance/js/DeleteOldFilesTest.js index 720b90f2..83d7c96d 100644 --- a/test/acceptance/js/DeleteOldFilesTest.js +++ b/test/acceptance/js/DeleteOldFilesTest.js @@ -8,53 +8,66 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const ClsiApp = require("./helpers/ClsiApp"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const ClsiApp = require('./helpers/ClsiApp') -describe("Deleting Old Files", function() { - before(function(done){ - this.request = { - resources: [{ - path: "main.tex", - content: `\ +describe('Deleting Old Files', function() { + before(function(done) { + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\begin{document} Hello world \\end{document}\ ` - } - ] - }; - return ClsiApp.ensureRunning(done); - }); + } + ] + } + return ClsiApp.ensureRunning(done) + }) - return describe("on first run", function() { - before(function(done) { - this.project_id = Client.randomId(); - return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); - }); + return describe('on first run', function() { + before(function(done) { + this.project_id = Client.randomId() + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) - it("should return a success status", function() { - return this.body.compile.status.should.equal("success"); - }); + it('should return a success status', function() { + return this.body.compile.status.should.equal('success') + }) - return describe("after file has been deleted", function() { - before(function(done) { - this.request.resources = []; - return Client.compile(this.project_id, this.request, (error, res, body) => { - this.error = error; - this.res = res; - this.body = body; - return done(); - }); - }); - - return it("should return a failure status", function() { - return this.body.compile.status.should.equal("failure"); - }); - }); - }); -}); + return describe('after file has been deleted', function() { + before(function(done) { + this.request.resources = [] + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + return it('should return a failure status', function() { + return this.body.compile.status.should.equal('failure') + }) + }) + }) +}) diff --git a/test/acceptance/js/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js index 4c3080f3..110b5d6f 100644 --- a/test/acceptance/js/ExampleDocumentTests.js +++ b/test/acceptance/js/ExampleDocumentTests.js @@ -15,176 +15,266 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const fs = require("fs"); -const ChildProcess = require("child_process"); -const ClsiApp = require("./helpers/ClsiApp"); -const logger = require("logger-sharelatex"); -const Path = require("path"); -const fixturePath = path => Path.normalize(__dirname + "/../fixtures/" + path); -const process = require("process"); -console.log(process.pid, process.ppid, process.getuid(),process.getgroups(), "PID"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const fs = require('fs') +const ChildProcess = require('child_process') +const ClsiApp = require('./helpers/ClsiApp') +const logger = require('logger-sharelatex') +const Path = require('path') +const fixturePath = path => Path.normalize(__dirname + '/../fixtures/' + path) +const process = require('process') +console.log( + process.pid, + process.ppid, + process.getuid(), + process.getgroups(), + 'PID' +) try { - console.log("creating tmp directory", fixturePath("tmp")); - fs.mkdirSync(fixturePath("tmp")); + console.log('creating tmp directory', fixturePath('tmp')) + fs.mkdirSync(fixturePath('tmp')) } catch (error) { - const err = error; - console.log(err, fixturePath("tmp"), "unable to create fixture tmp path"); + const err = error + console.log(err, fixturePath('tmp'), 'unable to create fixture tmp path') } -const MOCHA_LATEX_TIMEOUT = 60 * 1000; +const MOCHA_LATEX_TIMEOUT = 60 * 1000 const convertToPng = function(pdfPath, pngPath, callback) { - if (callback == null) { callback = function(error) {}; } - const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}`; - console.log("COMMAND"); - console.log(command); - const convert = ChildProcess.exec(command); - const stdout = ""; - convert.stdout.on("data", chunk => console.log("STDOUT", chunk.toString())); - convert.stderr.on("data", chunk => console.log("STDERR", chunk.toString())); - return convert.on("exit", () => callback()); -}; + if (callback == null) { + callback = function(error) {} + } + const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}` + console.log('COMMAND') + console.log(command) + const convert = ChildProcess.exec(command) + const stdout = '' + convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString())) + convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) + return convert.on('exit', () => callback()) +} const compare = function(originalPath, generatedPath, callback) { - if (callback == null) { callback = function(error, same) {}; } - const diff_file = `${fixturePath(generatedPath)}-diff.png`; - const proc = ChildProcess.exec(`compare -metric mae ${fixturePath(originalPath)} ${fixturePath(generatedPath)} ${diff_file}`); - let stderr = ""; - proc.stderr.on("data", chunk => stderr += chunk); - return proc.on("exit", () => { - if (stderr.trim() === "0 (0)") { - // remove output diff if test matches expected image - fs.unlink(diff_file, (err) => { - if (err) { - throw err; - } - }); - return callback(null, true); - } else { - console.log("compare result", stderr); - return callback(null, false); - } - }); -}; + if (callback == null) { + callback = function(error, same) {} + } + const diff_file = `${fixturePath(generatedPath)}-diff.png` + const proc = ChildProcess.exec( + `compare -metric mae ${fixturePath(originalPath)} ${fixturePath( + generatedPath + )} ${diff_file}` + ) + let stderr = '' + proc.stderr.on('data', chunk => (stderr += chunk)) + return proc.on('exit', () => { + if (stderr.trim() === '0 (0)') { + // remove output diff if test matches expected image + fs.unlink(diff_file, err => { + if (err) { + throw err + } + }) + return callback(null, true) + } else { + console.log('compare result', stderr) + return callback(null, false) + } + }) +} const checkPdfInfo = function(pdfPath, callback) { - if (callback == null) { callback = function(error, output) {}; } - const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`); - let stdout = ""; - proc.stdout.on("data", chunk => stdout += chunk); - proc.stderr.on("data", chunk => console.log("STDERR", chunk.toString())); - return proc.on("exit", () => { - if (stdout.match(/Optimized:\s+yes/)) { - return callback(null, true); - } else { - return callback(null, false); - } - }); -}; + if (callback == null) { + callback = function(error, output) {} + } + const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`) + let stdout = '' + proc.stdout.on('data', chunk => (stdout += chunk)) + proc.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) + return proc.on('exit', () => { + if (stdout.match(/Optimized:\s+yes/)) { + return callback(null, true) + } else { + return callback(null, false) + } + }) +} const compareMultiplePages = function(project_id, callback) { - if (callback == null) { callback = function(error) {}; } - var compareNext = function(page_no, callback) { - const path = `tmp/${project_id}-source-${page_no}.png`; - return fs.stat(fixturePath(path), (error, stat) => { - if (error != null) { - return callback(); - } else { - return compare(`tmp/${project_id}-source-${page_no}.png`, `tmp/${project_id}-generated-${page_no}.png`, (error, same) => { - if (error != null) { throw error; } - same.should.equal(true); - return compareNext(page_no + 1, callback); - }); - } - }); - }; - return compareNext(0, callback); -}; + if (callback == null) { + callback = function(error) {} + } + var compareNext = function(page_no, callback) { + const path = `tmp/${project_id}-source-${page_no}.png` + return fs.stat(fixturePath(path), (error, stat) => { + if (error != null) { + return callback() + } else { + return compare( + `tmp/${project_id}-source-${page_no}.png`, + `tmp/${project_id}-generated-${page_no}.png`, + (error, same) => { + if (error != null) { + throw error + } + same.should.equal(true) + return compareNext(page_no + 1, callback) + } + ) + } + }) + } + return compareNext(0, callback) +} const comparePdf = function(project_id, example_dir, callback) { - if (callback == null) { callback = function(error) {}; } - console.log("CONVERT"); - console.log(`tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`); - return convertToPng(`tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`, error => { - if (error != null) { throw error; } - return convertToPng(`examples/${example_dir}/output.pdf`, `tmp/${project_id}-source.png`, error => { - if (error != null) { throw error; } - return fs.stat(fixturePath(`tmp/${project_id}-source-0.png`), (error, stat) => { - if (error != null) { - return compare(`tmp/${project_id}-source.png`, `tmp/${project_id}-generated.png`, (error, same) => { - if (error != null) { throw error; } - same.should.equal(true); - return callback(); - }); - } else { - return compareMultiplePages(project_id, (error) => { - if (error != null) { throw error; } - return callback(); - }); - } - }); - }); - }); -}; + if (callback == null) { + callback = function(error) {} + } + console.log('CONVERT') + console.log(`tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`) + return convertToPng( + `tmp/${project_id}.pdf`, + `tmp/${project_id}-generated.png`, + error => { + if (error != null) { + throw error + } + return convertToPng( + `examples/${example_dir}/output.pdf`, + `tmp/${project_id}-source.png`, + error => { + if (error != null) { + throw error + } + return fs.stat( + fixturePath(`tmp/${project_id}-source-0.png`), + (error, stat) => { + if (error != null) { + return compare( + `tmp/${project_id}-source.png`, + `tmp/${project_id}-generated.png`, + (error, same) => { + if (error != null) { + throw error + } + same.should.equal(true) + return callback() + } + ) + } else { + return compareMultiplePages(project_id, error => { + if (error != null) { + throw error + } + return callback() + }) + } + } + ) + } + ) + } + ) +} const downloadAndComparePdf = function(project_id, example_dir, url, callback) { - if (callback == null) { callback = function(error) {}; } - const writeStream = fs.createWriteStream(fixturePath(`tmp/${project_id}.pdf`)); - request.get(url).pipe(writeStream); - console.log("writing file out", fixturePath(`tmp/${project_id}.pdf`)); - return writeStream.on("close", () => { - return checkPdfInfo(`tmp/${project_id}.pdf`, (error, optimised) => { - if (error != null) { throw error; } - optimised.should.equal(true); - return comparePdf(project_id, example_dir, callback); - }); - }); -}; - -Client.runServer(4242, fixturePath("examples")); - -describe("Example Documents", function() { - before(function(done) { return ChildProcess.exec("rm test/acceptance/fixtures/tmp/*").on("exit", () => ClsiApp.ensureRunning(done)); } - ); - + if (callback == null) { + callback = function(error) {} + } + const writeStream = fs.createWriteStream(fixturePath(`tmp/${project_id}.pdf`)) + request.get(url).pipe(writeStream) + console.log('writing file out', fixturePath(`tmp/${project_id}.pdf`)) + return writeStream.on('close', () => { + return checkPdfInfo(`tmp/${project_id}.pdf`, (error, optimised) => { + if (error != null) { + throw error + } + optimised.should.equal(true) + return comparePdf(project_id, example_dir, callback) + }) + }) +} - return Array.from(fs.readdirSync(fixturePath("examples"))).map((example_dir) => - (example_dir => - describe(example_dir, function() { - before(function() { - return this.project_id = Client.randomId() + "_" + example_dir; - }); +Client.runServer(4242, fixturePath('examples')) - it("should generate the correct pdf", function(done) { - this.timeout(MOCHA_LATEX_TIMEOUT); - return Client.compileDirectory(this.project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => { - if (error || (__guard__(body != null ? body.compile : undefined, x => x.status) === "failure")) { - console.log("DEBUG: error", error, "body", JSON.stringify(body)); - } - const pdf = Client.getOutputFile(body, "pdf"); - return downloadAndComparePdf(this.project_id, example_dir, pdf.url, done); - }); - }); +describe('Example Documents', function() { + before(function(done) { + return ChildProcess.exec('rm test/acceptance/fixtures/tmp/*').on( + 'exit', + () => ClsiApp.ensureRunning(done) + ) + }) - return it("should generate the correct pdf on the second run as well", function(done) { - this.timeout(MOCHA_LATEX_TIMEOUT); - return Client.compileDirectory(this.project_id, fixturePath("examples"), example_dir, 4242, (error, res, body) => { - if (error || (__guard__(body != null ? body.compile : undefined, x => x.status) === "failure")) { - console.log("DEBUG: error", error, "body", JSON.stringify(body)); - } - const pdf = Client.getOutputFile(body, "pdf"); - return downloadAndComparePdf(this.project_id, example_dir, pdf.url, done); - }); - }); - }) - )(example_dir)); -}); + return Array.from(fs.readdirSync(fixturePath('examples'))).map(example_dir => + (example_dir => + describe(example_dir, function() { + before(function() { + return (this.project_id = Client.randomId() + '_' + example_dir) + }) + it('should generate the correct pdf', function(done) { + this.timeout(MOCHA_LATEX_TIMEOUT) + return Client.compileDirectory( + this.project_id, + fixturePath('examples'), + example_dir, + 4242, + (error, res, body) => { + if ( + error || + __guard__( + body != null ? body.compile : undefined, + x => x.status + ) === 'failure' + ) { + console.log('DEBUG: error', error, 'body', JSON.stringify(body)) + } + const pdf = Client.getOutputFile(body, 'pdf') + return downloadAndComparePdf( + this.project_id, + example_dir, + pdf.url, + done + ) + } + ) + }) + return it('should generate the correct pdf on the second run as well', function(done) { + this.timeout(MOCHA_LATEX_TIMEOUT) + return Client.compileDirectory( + this.project_id, + fixturePath('examples'), + example_dir, + 4242, + (error, res, body) => { + if ( + error || + __guard__( + body != null ? body.compile : undefined, + x => x.status + ) === 'failure' + ) { + console.log('DEBUG: error', error, 'body', JSON.stringify(body)) + } + const pdf = Client.getOutputFile(body, 'pdf') + return downloadAndComparePdf( + this.project_id, + example_dir, + pdf.url, + done + ) + } + ) + }) + }))(example_dir) + ) +}) function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/test/acceptance/js/SimpleLatexFileTests.js b/test/acceptance/js/SimpleLatexFileTests.js index d774301d..447e1b63 100644 --- a/test/acceptance/js/SimpleLatexFileTests.js +++ b/test/acceptance/js/SimpleLatexFileTests.js @@ -8,55 +8,64 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const ClsiApp = require("./helpers/ClsiApp"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const ClsiApp = require('./helpers/ClsiApp') -describe("Simple LaTeX file", function() { - before(function(done) { - this.project_id = Client.randomId(); - this.request = { - resources: [{ - path: "main.tex", - content: `\ +describe('Simple LaTeX file', function() { + before(function(done) { + this.project_id = Client.randomId() + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\begin{document} Hello world \\end{document}\ ` - } - ] - }; - return ClsiApp.ensureRunning(() => { - return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); - }); - }); + } + ] + } + return ClsiApp.ensureRunning(() => { + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + }) - it("should return the PDF", function() { - const pdf = Client.getOutputFile(this.body, "pdf"); - return pdf.type.should.equal("pdf"); - }); - - it("should return the log", function() { - const log = Client.getOutputFile(this.body, "log"); - return log.type.should.equal("log"); - }); + it('should return the PDF', function() { + const pdf = Client.getOutputFile(this.body, 'pdf') + return pdf.type.should.equal('pdf') + }) - it("should provide the pdf for download", function(done) { - const pdf = Client.getOutputFile(this.body, "pdf"); - return request.get(pdf.url, (error, res, body) => { - res.statusCode.should.equal(200); - return done(); - }); - }); - - return it("should provide the log for download", function(done) { - const log = Client.getOutputFile(this.body, "pdf"); - return request.get(log.url, (error, res, body) => { - res.statusCode.should.equal(200); - return done(); - }); - }); -}); - + it('should return the log', function() { + const log = Client.getOutputFile(this.body, 'log') + return log.type.should.equal('log') + }) + + it('should provide the pdf for download', function(done) { + const pdf = Client.getOutputFile(this.body, 'pdf') + return request.get(pdf.url, (error, res, body) => { + res.statusCode.should.equal(200) + return done() + }) + }) + + return it('should provide the log for download', function(done) { + const log = Client.getOutputFile(this.body, 'pdf') + return request.get(log.url, (error, res, body) => { + res.statusCode.should.equal(200) + return done() + }) + }) +}) diff --git a/test/acceptance/js/SynctexTests.js b/test/acceptance/js/SynctexTests.js index d8879ebc..4860c604 100644 --- a/test/acceptance/js/SynctexTests.js +++ b/test/acceptance/js/SynctexTests.js @@ -9,53 +9,83 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const { expect } = require("chai"); -const ClsiApp = require("./helpers/ClsiApp"); -const crypto = require("crypto"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const { expect } = require('chai') +const ClsiApp = require('./helpers/ClsiApp') +const crypto = require('crypto') -describe("Syncing", function() { - before(function(done) { - const content = `\ +describe('Syncing', function() { + before(function(done) { + const content = `\ \\documentclass{article} \\begin{document} Hello world \\end{document}\ -`; - this.request = { - resources: [{ - path: "main.tex", - content - } - ] - }; - this.project_id = Client.randomId(); - return ClsiApp.ensureRunning(() => { - return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); - }); - }); +` + this.request = { + resources: [ + { + path: 'main.tex', + content + } + ] + } + this.project_id = Client.randomId() + return ClsiApp.ensureRunning(() => { + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + }) - describe("from code to pdf", function() { return it("should return the correct location", function(done) { - return Client.syncFromCode(this.project_id, "main.tex", 3, 5, (error, pdfPositions) => { - if (error != null) { throw error; } - expect(pdfPositions).to.deep.equal({ - pdf: [ { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] - }); - return done(); - }); - }); } - ); + describe('from code to pdf', function() { + return it('should return the correct location', function(done) { + return Client.syncFromCode( + this.project_id, + 'main.tex', + 3, + 5, + (error, pdfPositions) => { + if (error != null) { + throw error + } + expect(pdfPositions).to.deep.equal({ + pdf: [ + { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } + ] + }) + return done() + } + ) + }) + }) - return describe("from pdf to code", function() { return it("should return the correct location", function(done) { - return Client.syncFromPdf(this.project_id, 1, 100, 200, (error, codePositions) => { - if (error != null) { throw error; } - expect(codePositions).to.deep.equal({ - code: [ { file: 'main.tex', line: 3, column: -1 } ] - }); - return done(); - }); - }); } - ); -}); + return describe('from pdf to code', function() { + return it('should return the correct location', function(done) { + return Client.syncFromPdf( + this.project_id, + 1, + 100, + 200, + (error, codePositions) => { + if (error != null) { + throw error + } + expect(codePositions).to.deep.equal({ + code: [{ file: 'main.tex', line: 3, column: -1 }] + }) + return done() + } + ) + }) + }) +}) diff --git a/test/acceptance/js/TimeoutTests.js b/test/acceptance/js/TimeoutTests.js index 7f8f8483..f6812e8c 100644 --- a/test/acceptance/js/TimeoutTests.js +++ b/test/acceptance/js/TimeoutTests.js @@ -8,46 +8,55 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const ClsiApp = require("./helpers/ClsiApp"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const ClsiApp = require('./helpers/ClsiApp') - -describe("Timed out compile", function() { - before(function(done) { - this.request = { - options: { - timeout: 10 - }, // seconds - resources: [{ - path: "main.tex", - content: `\ +describe('Timed out compile', function() { + before(function(done) { + this.request = { + options: { + timeout: 10 + }, // seconds + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\begin{document} \\def\\x{Hello!\\par\\x} \\x \\end{document}\ ` - } - ] - }; - this.project_id = Client.randomId(); - return ClsiApp.ensureRunning(() => { - return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); - }); - }); + } + ] + } + this.project_id = Client.randomId() + return ClsiApp.ensureRunning(() => { + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + }) - it("should return a timeout error", function() { - return this.body.compile.error.should.equal("container timed out"); - }); + it('should return a timeout error', function() { + return this.body.compile.error.should.equal('container timed out') + }) - it("should return a timedout status", function() { - return this.body.compile.status.should.equal("timedout"); - }); + it('should return a timedout status', function() { + return this.body.compile.status.should.equal('timedout') + }) - return it("should return the log output file name", function() { - const outputFilePaths = this.body.compile.outputFiles.map(x => x.path); - return outputFilePaths.should.include('output.log'); - }); -}); + return it('should return the log output file name', function() { + const outputFilePaths = this.body.compile.outputFiles.map(x => x.path) + return outputFilePaths.should.include('output.log') + }) +}) diff --git a/test/acceptance/js/UrlCachingTests.js b/test/acceptance/js/UrlCachingTests.js index 7bb0a205..4d624978 100644 --- a/test/acceptance/js/UrlCachingTests.js +++ b/test/acceptance/js/UrlCachingTests.js @@ -10,277 +10,364 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const sinon = require("sinon"); -const ClsiApp = require("./helpers/ClsiApp"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const sinon = require('sinon') +const ClsiApp = require('./helpers/ClsiApp') -const host = "localhost"; +const host = 'localhost' const Server = { - run() { - const express = require("express"); - const app = express(); + run() { + const express = require('express') + const app = express() - const staticServer = express.static(__dirname + "/../fixtures/"); - app.get("/:random_id/*", (req, res, next) => { - this.getFile(req.url); - req.url = `/${req.params[0]}`; - return staticServer(req, res, next); - }); + const staticServer = express.static(__dirname + '/../fixtures/') + app.get('/:random_id/*', (req, res, next) => { + this.getFile(req.url) + req.url = `/${req.params[0]}` + return staticServer(req, res, next) + }) - return app.listen(31415, host); - }, + return app.listen(31415, host) + }, - getFile() {}, + getFile() {}, - randomId() { - return Math.random().toString(16).slice(2); - } -}; + randomId() { + return Math.random() + .toString(16) + .slice(2) + } +} -Server.run(); +Server.run() -describe("Url Caching", function() { - describe("Downloading an image for the first time", function() { - before(function(done) { - this.project_id = Client.randomId(); - this.file = `${Server.randomId()}/lion.png`; - this.request = { - resources: [{ - path: "main.tex", - content: `\ +describe('Url Caching', function() { + describe('Downloading an image for the first time', function() { + before(function(done) { + this.project_id = Client.randomId() + this.file = `${Server.randomId()}/lion.png` + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\usepackage{graphicx} \\begin{document} \\includegraphics{lion.png} \\end{document}\ ` - }, { - path: "lion.png", - url: `http://${host}:31415/${this.file}` - }] - }; + }, + { + path: 'lion.png', + url: `http://${host}:31415/${this.file}` + } + ] + } - sinon.spy(Server, "getFile"); - return ClsiApp.ensureRunning(() => { - return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); - }); - }); + sinon.spy(Server, 'getFile') + return ClsiApp.ensureRunning(() => { + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + }) - afterEach(function() { return Server.getFile.restore(); }); + afterEach(function() { + return Server.getFile.restore() + }) - return it("should download the image", function() { - return Server.getFile - .calledWith(`/${this.file}`) - .should.equal(true); - }); - }); - - describe("When an image is in the cache and the last modified date is unchanged", function() { - before(function(done) { - this.project_id = Client.randomId(); - this.file = `${Server.randomId()}/lion.png`; - this.request = { - resources: [{ - path: "main.tex", - content: `\ + return it('should download the image', function() { + return Server.getFile.calledWith(`/${this.file}`).should.equal(true) + }) + }) + + describe('When an image is in the cache and the last modified date is unchanged', function() { + before(function(done) { + this.project_id = Client.randomId() + this.file = `${Server.randomId()}/lion.png` + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\usepackage{graphicx} \\begin{document} \\includegraphics{lion.png} \\end{document}\ ` - }, (this.image_resource = { - path: "lion.png", - url: `http://${host}:31415/${this.file}`, - modified: Date.now() - })] - }; + }, + (this.image_resource = { + path: 'lion.png', + url: `http://${host}:31415/${this.file}`, + modified: Date.now() + }) + ] + } - return Client.compile(this.project_id, this.request, (error, res, body) => { - this.error = error; - this.res = res; - this.body = body; - sinon.spy(Server, "getFile"); - return Client.compile(this.project_id, this.request, (error1, res1, body1) => { - this.error = error1; - this.res = res1; - this.body = body1; - return done(); - }); - }); - }); + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + sinon.spy(Server, 'getFile') + return Client.compile( + this.project_id, + this.request, + (error1, res1, body1) => { + this.error = error1 + this.res = res1 + this.body = body1 + return done() + } + ) + } + ) + }) - after(function() { return Server.getFile.restore(); }); + after(function() { + return Server.getFile.restore() + }) - return it("should not download the image again", function() { return Server.getFile.called.should.equal(false); }); - }); + return it('should not download the image again', function() { + return Server.getFile.called.should.equal(false) + }) + }) - describe("When an image is in the cache and the last modified date is advanced", function() { - before(function(done) { - this.project_id = Client.randomId(); - this.file = `${Server.randomId()}/lion.png`; - this.request = { - resources: [{ - path: "main.tex", - content: `\ + describe('When an image is in the cache and the last modified date is advanced', function() { + before(function(done) { + this.project_id = Client.randomId() + this.file = `${Server.randomId()}/lion.png` + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\usepackage{graphicx} \\begin{document} \\includegraphics{lion.png} \\end{document}\ ` - }, (this.image_resource = { - path: "lion.png", - url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - })] - }; + }, + (this.image_resource = { + path: 'lion.png', + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + }) + ] + } - return Client.compile(this.project_id, this.request, (error, res, body) => { - this.error = error; - this.res = res; - this.body = body; - sinon.spy(Server, "getFile"); - this.image_resource.modified = new Date(this.last_modified + 3000); - return Client.compile(this.project_id, this.request, (error1, res1, body1) => { - this.error = error1; - this.res = res1; - this.body = body1; - return done(); - }); - }); - }); + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + sinon.spy(Server, 'getFile') + this.image_resource.modified = new Date(this.last_modified + 3000) + return Client.compile( + this.project_id, + this.request, + (error1, res1, body1) => { + this.error = error1 + this.res = res1 + this.body = body1 + return done() + } + ) + } + ) + }) - afterEach(function() { return Server.getFile.restore(); }); + afterEach(function() { + return Server.getFile.restore() + }) - return it("should download the image again", function() { return Server.getFile.called.should.equal(true); }); - }); + return it('should download the image again', function() { + return Server.getFile.called.should.equal(true) + }) + }) - describe("When an image is in the cache and the last modified date is further in the past", function() { - before(function(done) { - this.project_id = Client.randomId(); - this.file = `${Server.randomId()}/lion.png`; - this.request = { - resources: [{ - path: "main.tex", - content: `\ + describe('When an image is in the cache and the last modified date is further in the past', function() { + before(function(done) { + this.project_id = Client.randomId() + this.file = `${Server.randomId()}/lion.png` + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\usepackage{graphicx} \\begin{document} \\includegraphics{lion.png} \\end{document}\ ` - }, (this.image_resource = { - path: "lion.png", - url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - })] - }; + }, + (this.image_resource = { + path: 'lion.png', + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + }) + ] + } - return Client.compile(this.project_id, this.request, (error, res, body) => { - this.error = error; - this.res = res; - this.body = body; - sinon.spy(Server, "getFile"); - this.image_resource.modified = new Date(this.last_modified - 3000); - return Client.compile(this.project_id, this.request, (error1, res1, body1) => { - this.error = error1; - this.res = res1; - this.body = body1; - return done(); - }); - }); - }); + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + sinon.spy(Server, 'getFile') + this.image_resource.modified = new Date(this.last_modified - 3000) + return Client.compile( + this.project_id, + this.request, + (error1, res1, body1) => { + this.error = error1 + this.res = res1 + this.body = body1 + return done() + } + ) + } + ) + }) - afterEach(function() { return Server.getFile.restore(); }); + afterEach(function() { + return Server.getFile.restore() + }) - return it("should not download the image again", function() { return Server.getFile.called.should.equal(false); }); - }); + return it('should not download the image again', function() { + return Server.getFile.called.should.equal(false) + }) + }) - describe("When an image is in the cache and the last modified date is not specified", function() { - before(function(done) { - this.project_id = Client.randomId(); - this.file = `${Server.randomId()}/lion.png`; - this.request = { - resources: [{ - path: "main.tex", - content: `\ + describe('When an image is in the cache and the last modified date is not specified', function() { + before(function(done) { + this.project_id = Client.randomId() + this.file = `${Server.randomId()}/lion.png` + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\usepackage{graphicx} \\begin{document} \\includegraphics{lion.png} \\end{document}\ ` - }, (this.image_resource = { - path: "lion.png", - url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - })] - }; + }, + (this.image_resource = { + path: 'lion.png', + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + }) + ] + } + + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + sinon.spy(Server, 'getFile') + delete this.image_resource.modified + return Client.compile( + this.project_id, + this.request, + (error1, res1, body1) => { + this.error = error1 + this.res = res1 + this.body = body1 + return done() + } + ) + } + ) + }) - return Client.compile(this.project_id, this.request, (error, res, body) => { - this.error = error; - this.res = res; - this.body = body; - sinon.spy(Server, "getFile"); - delete this.image_resource.modified; - return Client.compile(this.project_id, this.request, (error1, res1, body1) => { - this.error = error1; - this.res = res1; - this.body = body1; - return done(); - }); - }); - }); + afterEach(function() { + return Server.getFile.restore() + }) - afterEach(function() { return Server.getFile.restore(); }); + return it('should download the image again', function() { + return Server.getFile.called.should.equal(true) + }) + }) - return it("should download the image again", function() { return Server.getFile.called.should.equal(true); }); - }); - - return describe("After clearing the cache", function() { - before(function(done) { - this.project_id = Client.randomId(); - this.file = `${Server.randomId()}/lion.png`; - this.request = { - resources: [{ - path: "main.tex", - content: `\ + return describe('After clearing the cache', function() { + before(function(done) { + this.project_id = Client.randomId() + this.file = `${Server.randomId()}/lion.png` + this.request = { + resources: [ + { + path: 'main.tex', + content: `\ \\documentclass{article} \\usepackage{graphicx} \\begin{document} \\includegraphics{lion.png} \\end{document}\ ` - }, (this.image_resource = { - path: "lion.png", - url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - })] - }; + }, + (this.image_resource = { + path: 'lion.png', + url: `http://${host}:31415/${this.file}`, + modified: (this.last_modified = Date.now()) + }) + ] + } - return Client.compile(this.project_id, this.request, error => { - if (error != null) { throw error; } - return Client.clearCache(this.project_id, (error, res, body) => { - if (error != null) { throw error; } - sinon.spy(Server, "getFile"); - return Client.compile(this.project_id, this.request, (error1, res1, body1) => { - this.error = error1; - this.res = res1; - this.body = body1; - return done(); - }); - }); - }); - }); + return Client.compile(this.project_id, this.request, error => { + if (error != null) { + throw error + } + return Client.clearCache(this.project_id, (error, res, body) => { + if (error != null) { + throw error + } + sinon.spy(Server, 'getFile') + return Client.compile( + this.project_id, + this.request, + (error1, res1, body1) => { + this.error = error1 + this.res = res1 + this.body = body1 + return done() + } + ) + }) + }) + }) - afterEach(function() { return Server.getFile.restore(); }); + afterEach(function() { + return Server.getFile.restore() + }) - return it("should download the image again", function() { return Server.getFile.called.should.equal(true); }); - }); -}); - - + return it('should download the image again', function() { + return Server.getFile.called.should.equal(true) + }) + }) +}) diff --git a/test/acceptance/js/WordcountTests.js b/test/acceptance/js/WordcountTests.js index 2f81e139..87218570 100644 --- a/test/acceptance/js/WordcountTests.js +++ b/test/acceptance/js/WordcountTests.js @@ -9,48 +9,64 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Client = require("./helpers/Client"); -const request = require("request"); -require("chai").should(); -const { expect } = require("chai"); -const path = require("path"); -const fs = require("fs"); -const ClsiApp = require("./helpers/ClsiApp"); +const Client = require('./helpers/Client') +const request = require('request') +require('chai').should() +const { expect } = require('chai') +const path = require('path') +const fs = require('fs') +const ClsiApp = require('./helpers/ClsiApp') -describe("Syncing", function() { - before(function(done) { - this.request = { - resources: [{ - path: "main.tex", - content: fs.readFileSync(path.join(__dirname,"../fixtures/naugty_strings.txt"),"utf-8") - } - ] - }; - this.project_id = Client.randomId(); - return ClsiApp.ensureRunning(() => { - return Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error; this.res = res; this.body = body; return done(); }); - }); - }); +describe('Syncing', function() { + before(function(done) { + this.request = { + resources: [ + { + path: 'main.tex', + content: fs.readFileSync( + path.join(__dirname, '../fixtures/naugty_strings.txt'), + 'utf-8' + ) + } + ] + } + this.project_id = Client.randomId() + return ClsiApp.ensureRunning(() => { + return Client.compile( + this.project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + }) - return describe("wordcount file", function() { return it("should return wordcount info", function(done) { - return Client.wordcount(this.project_id, "main.tex", (error, result) => { - if (error != null) { throw error; } - expect(result).to.deep.equal({ - texcount: { - encode: "utf8", - textWords: 2281, - headWords: 2, - outside: 0, - headers: 2, - elements: 0, - mathInline: 6, - mathDisplay: 0, - errors: 0, - messages: "" - } - }); - return done(); - }); - }); } - ); -}); + return describe('wordcount file', function() { + return it('should return wordcount info', function(done) { + return Client.wordcount(this.project_id, 'main.tex', (error, result) => { + if (error != null) { + throw error + } + expect(result).to.deep.equal({ + texcount: { + encode: 'utf8', + textWords: 2281, + headWords: 2, + outside: 0, + headers: 2, + elements: 0, + mathInline: 6, + mathDisplay: 0, + errors: 0, + messages: '' + } + }) + return done() + }) + }) + }) +}) diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index 50e75d61..9f430e35 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -12,143 +12,197 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let Client; -const request = require("request"); -const fs = require("fs"); -const Settings = require("settings-sharelatex"); +let Client +const request = require('request') +const fs = require('fs') +const Settings = require('settings-sharelatex') -const host = "localhost"; +const host = 'localhost' -module.exports = (Client = { - host: Settings.apis.clsi.url, +module.exports = Client = { + host: Settings.apis.clsi.url, - randomId() { - return Math.random().toString(16).slice(2); - }, + randomId() { + return Math.random() + .toString(16) + .slice(2) + }, - compile(project_id, data, callback) { - if (callback == null) { callback = function(error, res, body) {}; } - return request.post({ - url: `${this.host}/project/${project_id}/compile`, - json: { - compile: data - } - }, callback); - }, + compile(project_id, data, callback) { + if (callback == null) { + callback = function(error, res, body) {} + } + return request.post( + { + url: `${this.host}/project/${project_id}/compile`, + json: { + compile: data + } + }, + callback + ) + }, - clearCache(project_id, callback) { - if (callback == null) { callback = function(error, res, body) {}; } - return request.del(`${this.host}/project/${project_id}`, callback); - }, + clearCache(project_id, callback) { + if (callback == null) { + callback = function(error, res, body) {} + } + return request.del(`${this.host}/project/${project_id}`, callback) + }, - getOutputFile(response, type) { - for (const file of Array.from(response.compile.outputFiles)) { - if ((file.type === type) && file.url.match(`output.${type}`)) { - return file; - } - } - return null; - }, + getOutputFile(response, type) { + for (const file of Array.from(response.compile.outputFiles)) { + if (file.type === type && file.url.match(`output.${type}`)) { + return file + } + } + return null + }, - runServer(port, directory) { - const express = require("express"); - const app = express(); - app.use(express.static(directory)); - console.log("starting test server on", port, host); - return app.listen(port, host).on("error", (error) => { - console.error("error starting server:", error.message); - return process.exit(1); - }); - }, + runServer(port, directory) { + const express = require('express') + const app = express() + app.use(express.static(directory)) + console.log('starting test server on', port, host) + return app.listen(port, host).on('error', error => { + console.error('error starting server:', error.message) + return process.exit(1) + }) + }, + syncFromCode(project_id, file, line, column, callback) { + if (callback == null) { + callback = function(error, pdfPositions) {} + } + return request.get( + { + url: `${this.host}/project/${project_id}/sync/code`, + qs: { + file, + line, + column + } + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return callback(null, JSON.parse(body)) + } + ) + }, - syncFromCode(project_id, file, line, column, callback) { - if (callback == null) { callback = function(error, pdfPositions) {}; } - return request.get({ - url: `${this.host}/project/${project_id}/sync/code`, - qs: { - file, - line, - column - } - }, (error, response, body) => { - if (error != null) { return callback(error); } - return callback(null, JSON.parse(body)); - }); - }, + syncFromPdf(project_id, page, h, v, callback) { + if (callback == null) { + callback = function(error, pdfPositions) {} + } + return request.get( + { + url: `${this.host}/project/${project_id}/sync/pdf`, + qs: { + page, + h, + v + } + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return callback(null, JSON.parse(body)) + } + ) + }, - syncFromPdf(project_id, page, h, v, callback) { - if (callback == null) { callback = function(error, pdfPositions) {}; } - return request.get({ - url: `${this.host}/project/${project_id}/sync/pdf`, - qs: { - page, - h, v - } - }, (error, response, body) => { - if (error != null) { return callback(error); } - return callback(null, JSON.parse(body)); - }); - }, + compileDirectory(project_id, baseDirectory, directory, serverPort, callback) { + if (callback == null) { + callback = function(error, res, body) {} + } + const resources = [] + let entities = fs.readdirSync(`${baseDirectory}/${directory}`) + let rootResourcePath = 'main.tex' + while (entities.length > 0) { + var entity = entities.pop() + const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`) + if (stat.isDirectory()) { + entities = entities.concat( + fs + .readdirSync(`${baseDirectory}/${directory}/${entity}`) + .map(subEntity => { + if (subEntity === 'main.tex') { + rootResourcePath = `${entity}/${subEntity}` + } + return `${entity}/${subEntity}` + }) + ) + } else if (stat.isFile() && entity !== 'output.pdf') { + const extension = entity.split('.').pop() + if ( + [ + 'tex', + 'bib', + 'cls', + 'sty', + 'pdf_tex', + 'Rtex', + 'ist', + 'md', + 'Rmd' + ].indexOf(extension) > -1 + ) { + resources.push({ + path: entity, + content: fs + .readFileSync(`${baseDirectory}/${directory}/${entity}`) + .toString() + }) + } else if ( + ['eps', 'ttf', 'png', 'jpg', 'pdf', 'jpeg'].indexOf(extension) > -1 + ) { + resources.push({ + path: entity, + url: `http://${host}:${serverPort}/${directory}/${entity}`, + modified: stat.mtime + }) + } + } + } - compileDirectory(project_id, baseDirectory, directory, serverPort, callback) { - if (callback == null) { callback = function(error, res, body) {}; } - const resources = []; - let entities = fs.readdirSync(`${baseDirectory}/${directory}`); - let rootResourcePath = "main.tex"; - while (entities.length > 0) { - var entity = entities.pop(); - const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`); - if (stat.isDirectory()) { - entities = entities.concat(fs.readdirSync(`${baseDirectory}/${directory}/${entity}`).map((subEntity) => { - if (subEntity === "main.tex") { - rootResourcePath = `${entity}/${subEntity}`; - } - return `${entity}/${subEntity}`; - }) - ); - } else if (stat.isFile() && (entity !== "output.pdf")) { - const extension = entity.split(".").pop(); - if (["tex", "bib", "cls", "sty", "pdf_tex", "Rtex", "ist", "md", "Rmd"].indexOf(extension) > -1) { - resources.push({ - path: entity, - content: fs.readFileSync(`${baseDirectory}/${directory}/${entity}`).toString() - }); - } else if (["eps", "ttf", "png", "jpg", "pdf", "jpeg"].indexOf(extension) > -1) { - resources.push({ - path: entity, - url: `http://${host}:${serverPort}/${directory}/${entity}`, - modified: stat.mtime - }); - } - } - } + return fs.readFile( + `${baseDirectory}/${directory}/options.json`, + (error, body) => { + const req = { + resources, + rootResourcePath + } - return fs.readFile(`${baseDirectory}/${directory}/options.json`, (error, body) => { - const req = { - resources, - rootResourcePath - }; + if (error == null) { + body = JSON.parse(body) + req.options = body + } - if ((error == null)) { - body = JSON.parse(body); - req.options = body; - } + return this.compile(project_id, req, callback) + } + ) + }, - return this.compile(project_id, req, callback); - }); - }, - - wordcount(project_id, file, callback) { - if (callback == null) { callback = function(error, pdfPositions) {}; } - return request.get({ - url: `${this.host}/project/${project_id}/wordcount`, - qs: { - file - } - }, (error, response, body) => { - if (error != null) { return callback(error); } - return callback(null, JSON.parse(body)); - }); - } -}); + wordcount(project_id, file, callback) { + if (callback == null) { + callback = function(error, pdfPositions) {} + } + return request.get( + { + url: `${this.host}/project/${project_id}/wordcount`, + qs: { + file + } + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return callback(null, JSON.parse(body)) + } + ) + } +} diff --git a/test/acceptance/js/helpers/ClsiApp.js b/test/acceptance/js/helpers/ClsiApp.js index bd3222df..f8038460 100644 --- a/test/acceptance/js/helpers/ClsiApp.js +++ b/test/acceptance/js/helpers/ClsiApp.js @@ -12,40 +12,53 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const app = require('../../../../app'); -require("logger-sharelatex").logger.level("info"); -const logger = require("logger-sharelatex"); -const Settings = require("settings-sharelatex"); +const app = require('../../../../app') +require('logger-sharelatex').logger.level('info') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') module.exports = { - running: false, - initing: false, - callbacks: [], - ensureRunning(callback) { - if (callback == null) { callback = function(error) {}; } - if (this.running) { - return callback(); - } else if (this.initing) { - return this.callbacks.push(callback); - } else { - this.initing = true; - this.callbacks.push(callback); - return app.listen(__guard__(Settings.internal != null ? Settings.internal.clsi : undefined, x => x.port), "localhost", error => { - if (error != null) { throw error; } - this.running = true; - logger.log("clsi running in dev mode"); + running: false, + initing: false, + callbacks: [], + ensureRunning(callback) { + if (callback == null) { + callback = function(error) {} + } + if (this.running) { + return callback() + } else if (this.initing) { + return this.callbacks.push(callback) + } else { + this.initing = true + this.callbacks.push(callback) + return app.listen( + __guard__( + Settings.internal != null ? Settings.internal.clsi : undefined, + x => x.port + ), + 'localhost', + error => { + if (error != null) { + throw error + } + this.running = true + logger.log('clsi running in dev mode') - return (() => { - const result = []; - for (callback of Array.from(this.callbacks)) { - result.push(callback()); - } - return result; - })(); - }); - } - } -}; + return (() => { + const result = [] + for (callback of Array.from(this.callbacks)) { + result.push(callback()) + } + return result + })() + } + ) + } + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} From 8694fce0c95b769f169844f0f1f6137304ed504c Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:16:15 +0100 Subject: [PATCH 485/709] decaffeinate: rename individual coffee files to js files --- app.coffee => app.js | 0 config/{settings.defaults.coffee => settings.defaults.js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app.coffee => app.js (100%) rename config/{settings.defaults.coffee => settings.defaults.js} (100%) diff --git a/app.coffee b/app.js similarity index 100% rename from app.coffee rename to app.js diff --git a/config/settings.defaults.coffee b/config/settings.defaults.js similarity index 100% rename from config/settings.defaults.coffee rename to config/settings.defaults.js From 62d20ee5f087d652fa54ad4aa0073f62fe3f8c24 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:16:17 +0100 Subject: [PATCH 486/709] decaffeinate: convert individual files to js --- app.js | 518 ++++++++++++++++++++---------------- config/settings.defaults.js | 122 +++++---- 2 files changed, 356 insertions(+), 284 deletions(-) diff --git a/app.js b/app.js index 9bcdfebc..99427da2 100644 --- a/app.js +++ b/app.js @@ -1,244 +1,298 @@ -Metrics = require "metrics-sharelatex" -Metrics.initialize("clsi") - -CompileController = require "./app/js/CompileController" -Settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -logger.initialize("clsi") -if Settings.sentry?.dsn? - logger.initializeErrorReporting(Settings.sentry.dsn) - -smokeTest = require "smoke-test-sharelatex" -ContentTypeMapper = require "./app/js/ContentTypeMapper" -Errors = require './app/js/Errors' - -Path = require "path" -fs = require "fs" - - -Metrics.open_sockets.monitor(logger) -Metrics.memory.monitor(logger) - -ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager" -OutputCacheManager = require "./app/js/OutputCacheManager" - -require("./app/js/db").sync() - -express = require "express" -bodyParser = require "body-parser" -app = express() - -Metrics.injectMetricsRoute(app) -app.use Metrics.http.monitor(logger) - -# Compile requests can take longer than the default two -# minutes (including file download time), so bump up the -# timeout a bit. -TIMEOUT = 10 * 60 * 1000 -app.use (req, res, next) -> - req.setTimeout TIMEOUT - res.setTimeout TIMEOUT - res.removeHeader("X-Powered-By") - next() - -app.param 'project_id', (req, res, next, project_id) -> - if project_id?.match /^[a-zA-Z0-9_-]+$/ - next() - else - next new Error("invalid project id") - -app.param 'user_id', (req, res, next, user_id) -> - if user_id?.match /^[0-9a-f]{24}$/ - next() - else - next new Error("invalid user id") - -app.param 'build_id', (req, res, next, build_id) -> - if build_id?.match OutputCacheManager.BUILD_REGEX - next() - else - next new Error("invalid build id #{build_id}") - - -app.post "/project/:project_id/compile", bodyParser.json(limit: Settings.compileSizeLimit), 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 -app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf -app.get "/project/:project_id/wordcount", CompileController.wordcount -app.get "/project/:project_id/status", CompileController.status - -# Per-user containers -app.post "/project/:project_id/user/:user_id/compile", bodyParser.json(limit: Settings.compileSizeLimit), 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 -app.get "/project/:project_id/user/:user_id/sync/pdf", CompileController.syncFromPdf -app.get "/project/:project_id/user/:user_id/wordcount", CompileController.wordcount - -ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks" - -# create a static server which does not allow access to any symlinks -# avoids possible mismatch of root directory between middleware check -# and serving the files -staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHeaders: (res, path, stat) -> - if Path.basename(path) == "output.pdf" - # Calculate an etag in the same way as nginx - # https://github.com/tj/send/issues/65 - etag = (path, stat) -> - '"' + Math.ceil(+stat.mtime / 1000).toString(16) + +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let tenMinutes; +const Metrics = require("metrics-sharelatex"); +Metrics.initialize("clsi"); + +const CompileController = require("./app/js/CompileController"); +const Settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); +logger.initialize("clsi"); +if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { + logger.initializeErrorReporting(Settings.sentry.dsn); +} + +const smokeTest = require("smoke-test-sharelatex"); +const ContentTypeMapper = require("./app/js/ContentTypeMapper"); +const Errors = require('./app/js/Errors'); + +const Path = require("path"); +const fs = require("fs"); + + +Metrics.open_sockets.monitor(logger); +Metrics.memory.monitor(logger); + +const ProjectPersistenceManager = require("./app/js/ProjectPersistenceManager"); +const OutputCacheManager = require("./app/js/OutputCacheManager"); + +require("./app/js/db").sync(); + +const express = require("express"); +const bodyParser = require("body-parser"); +const app = express(); + +Metrics.injectMetricsRoute(app); +app.use(Metrics.http.monitor(logger)); + +// Compile requests can take longer than the default two +// minutes (including file download time), so bump up the +// timeout a bit. +const TIMEOUT = 10 * 60 * 1000; +app.use(function(req, res, next) { + req.setTimeout(TIMEOUT); + res.setTimeout(TIMEOUT); + res.removeHeader("X-Powered-By"); + return next(); +}); + +app.param('project_id', function(req, res, next, project_id) { + if ((project_id != null ? project_id.match(/^[a-zA-Z0-9_-]+$/) : undefined)) { + return next(); + } else { + return next(new Error("invalid project id")); + } +}); + +app.param('user_id', function(req, res, next, user_id) { + if ((user_id != null ? user_id.match(/^[0-9a-f]{24}$/) : undefined)) { + return next(); + } else { + return next(new Error("invalid user id")); + } +}); + +app.param('build_id', function(req, res, next, build_id) { + if ((build_id != null ? build_id.match(OutputCacheManager.BUILD_REGEX) : undefined)) { + return next(); + } else { + return next(new Error(`invalid build id ${build_id}`)); + } +}); + + +app.post("/project/:project_id/compile", bodyParser.json({limit: Settings.compileSizeLimit}), 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); +app.get("/project/:project_id/sync/pdf", CompileController.syncFromPdf); +app.get("/project/:project_id/wordcount", CompileController.wordcount); +app.get("/project/:project_id/status", CompileController.status); + +// Per-user containers +app.post("/project/:project_id/user/:user_id/compile", bodyParser.json({limit: Settings.compileSizeLimit}), 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); +app.get("/project/:project_id/user/:user_id/sync/pdf", CompileController.syncFromPdf); +app.get("/project/:project_id/user/:user_id/wordcount", CompileController.wordcount); + +const ForbidSymlinks = require("./app/js/StaticServerForbidSymlinks"); + +// create a static server which does not allow access to any symlinks +// avoids possible mismatch of root directory between middleware check +// and serving the files +const staticServer = ForbidSymlinks(express.static, Settings.path.compilesDir, { setHeaders(res, path, stat) { + if (Path.basename(path) === "output.pdf") { + // Calculate an etag in the same way as nginx + // https://github.com/tj/send/issues/65 + const etag = (path, stat) => + `"${Math.ceil(+stat.mtime / 1000).toString(16)}` + '-' + Number(stat.size).toString(16) + '"' - res.set("Etag", etag(path, stat)) - res.set("Content-Type", ContentTypeMapper.map(path)) - -app.get "/project/:project_id/user/:user_id/build/:build_id/output/*", (req, res, next) -> - # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = "/#{req.params.project_id}-#{req.params.user_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}") - staticServer(req, res, next) - -app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) -> - # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}") - staticServer(req, res, next) - -app.get "/project/:project_id/user/:user_id/output/*", (req, res, next) -> - # for specific user get the path to the top level file - req.url = "/#{req.params.project_id}-#{req.params.user_id}/#{req.params[0]}" - staticServer(req, res, next) - -app.get "/project/:project_id/output/*", (req, res, next) -> - if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX) - # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.query.build, "/#{req.params[0]}") - else - req.url = "/#{req.params.project_id}/#{req.params[0]}" - staticServer(req, res, next) - -app.get "/oops", (req, res, next) -> - logger.error {err: "hello"}, "test error" - res.send "error\n" - - -app.get "/status", (req, res, next) -> - res.send "CLSI is alive\n" - -resCacher = - contentType:(@setContentType)-> - send:(@code, @body)-> - - #default the server to be down - code:500 - body:{} + ; + res.set("Etag", etag(path, stat)); + } + return res.set("Content-Type", ContentTypeMapper.map(path)); +} +} +); + +app.get("/project/:project_id/user/:user_id/build/:build_id/output/*", function(req, res, next) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = `/${req.params.project_id}-${req.params.user_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`); + return staticServer(req, res, next); +}); + +app.get("/project/:project_id/build/:build_id/output/*", function(req, res, next) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = `/${req.params.project_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`); + return staticServer(req, res, next); +}); + +app.get("/project/:project_id/user/:user_id/output/*", function(req, res, next) { + // for specific user get the path to the top level file + req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}`; + return staticServer(req, res, next); +}); + +app.get("/project/:project_id/output/*", function(req, res, next) { + if (((req.query != null ? req.query.build : undefined) != null) && req.query.build.match(OutputCacheManager.BUILD_REGEX)) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = `/${req.params.project_id}/` + OutputCacheManager.path(req.query.build, `/${req.params[0]}`); + } else { + req.url = `/${req.params.project_id}/${req.params[0]}`; + } + return staticServer(req, res, next); +}); + +app.get("/oops", function(req, res, next) { + logger.error({err: "hello"}, "test error"); + return res.send("error\n"); +}); + + +app.get("/status", (req, res, next) => res.send("CLSI is alive\n")); + +const resCacher = { + contentType(setContentType){ + this.setContentType = setContentType; + }, + send(code, body){ + this.code = code; + this.body = body; + }, + + //default the server to be down + code:500, + body:{}, setContentType:"application/json" - -if Settings.smokeTest - do runSmokeTest = -> - logger.log("running smoke tests") - smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher) - setTimeout(runSmokeTest, 30 * 1000) - -app.get "/health_check", (req, res)-> - res.contentType(resCacher?.setContentType) - res.status(resCacher?.code).send(resCacher?.body) - -app.get "/smoke_test_force", (req, res)-> - smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res) - -profiler = require "v8-profiler-node8" -app.get "/profile", (req, res) -> - time = parseInt(req.query.time || "1000") - profiler.startProfiling("test") - setTimeout () -> - profile = profiler.stopProfiling("test") - res.json(profile) - , time - -app.get "/heapdump", (req, res)-> - require('heapdump').writeSnapshot '/tmp/' + Date.now() + '.clsi.heapsnapshot', (err, filename)-> - res.send filename - -app.use (error, req, res, next) -> - if error instanceof Errors.NotFoundError - logger.warn {err: error, url: req.url}, "not found error" - return res.sendStatus(404) - else - logger.error {err: error, url: req.url}, "server error" - res.sendStatus(error?.statusCode || 500) - -net = require "net" -os = require "os" - -STATE = "up" - - -loadTcpServer = net.createServer (socket) -> - socket.on "error", (err)-> - if err.code == "ECONNRESET" - # this always comes up, we don't know why - return - logger.err err:err, "error with socket on load check" - socket.destroy() +}; + +if (Settings.smokeTest) { + let runSmokeTest; + (runSmokeTest = function() { + logger.log("running smoke tests"); + smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher); + return setTimeout(runSmokeTest, 30 * 1000); + })(); +} + +app.get("/health_check", function(req, res){ + res.contentType(resCacher != null ? resCacher.setContentType : undefined); + return res.status(resCacher != null ? resCacher.code : undefined).send(resCacher != null ? resCacher.body : undefined); +}); + +app.get("/smoke_test_force", (req, res)=> smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res)); + +const profiler = require("v8-profiler-node8"); +app.get("/profile", function(req, res) { + const time = parseInt(req.query.time || "1000"); + profiler.startProfiling("test"); + return setTimeout(function() { + const profile = profiler.stopProfiling("test"); + return res.json(profile); + } + , time); +}); + +app.get("/heapdump", (req, res)=> + require('heapdump').writeSnapshot(`/tmp/${Date.now()}.clsi.heapsnapshot`, (err, filename)=> res.send(filename)) +); + +app.use(function(error, req, res, next) { + if (error instanceof Errors.NotFoundError) { + logger.warn({err: error, url: req.url}, "not found error"); + return res.sendStatus(404); + } else { + logger.error({err: error, url: req.url}, "server error"); + return res.sendStatus((error != null ? error.statusCode : undefined) || 500); + } +}); + +const net = require("net"); +const os = require("os"); + +let STATE = "up"; + + +const loadTcpServer = net.createServer(function(socket) { + socket.on("error", function(err){ + if (err.code === "ECONNRESET") { + // this always comes up, we don't know why + return; + } + logger.err({err}, "error with socket on load check"); + return socket.destroy(); + }); - if STATE == "up" and Settings.internal.load_balancer_agent.report_load - currentLoad = os.loadavg()[0] - - # staging clis's have 1 cpu core only - if os.cpus().length == 1 - availableWorkingCpus = 1 - else - availableWorkingCpus = os.cpus().length - 1 - - freeLoad = availableWorkingCpus - currentLoad - freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) - if freeLoadPercentage <= 0 - freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects to different servers - socket.write("up, #{freeLoadPercentage}%\n", "ASCII") - socket.end() - else - socket.write("#{STATE}\n", "ASCII") - socket.end() - -loadHttpServer = express() - -loadHttpServer.post "/state/up", (req, res, next) -> - STATE = "up" - logger.info "getting message to set server to down" - res.sendStatus 204 - -loadHttpServer.post "/state/down", (req, res, next) -> - STATE = "down" - logger.info "getting message to set server to down" - res.sendStatus 204 - -loadHttpServer.post "/state/maint", (req, res, next) -> - STATE = "maint" - logger.info "getting message to set server to maint" - res.sendStatus 204 + if ((STATE === "up") && Settings.internal.load_balancer_agent.report_load) { + let availableWorkingCpus; + const currentLoad = os.loadavg()[0]; + + // staging clis's have 1 cpu core only + if (os.cpus().length === 1) { + availableWorkingCpus = 1; + } else { + availableWorkingCpus = os.cpus().length - 1; + } + + const freeLoad = availableWorkingCpus - currentLoad; + let freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100); + if (freeLoadPercentage <= 0) { + freeLoadPercentage = 1; // when its 0 the server is set to drain and will move projects to different servers + } + socket.write(`up, ${freeLoadPercentage}%\n`, "ASCII"); + return socket.end(); + } else { + socket.write(`${STATE}\n`, "ASCII"); + return socket.end(); + } +}); + +const loadHttpServer = express(); + +loadHttpServer.post("/state/up", function(req, res, next) { + STATE = "up"; + logger.info("getting message to set server to down"); + return res.sendStatus(204); +}); + +loadHttpServer.post("/state/down", function(req, res, next) { + STATE = "down"; + logger.info("getting message to set server to down"); + return res.sendStatus(204); +}); + +loadHttpServer.post("/state/maint", function(req, res, next) { + STATE = "maint"; + logger.info("getting message to set server to maint"); + return res.sendStatus(204); +}); -port = (Settings.internal?.clsi?.port or 3013) -host = (Settings.internal?.clsi?.host or "localhost") +const port = (__guard__(Settings.internal != null ? Settings.internal.clsi : undefined, x => x.port) || 3013); +const host = (__guard__(Settings.internal != null ? Settings.internal.clsi : undefined, x1 => x1.host) || "localhost"); + +const load_tcp_port = Settings.internal.load_balancer_agent.load_port; +const load_http_port = Settings.internal.load_balancer_agent.local_port; -load_tcp_port = Settings.internal.load_balancer_agent.load_port -load_http_port = Settings.internal.load_balancer_agent.local_port +if (!module.parent) { // Called directly + app.listen(port, host, error => logger.info(`CLSI starting up, listening on ${host}:${port}`)); -if !module.parent # Called directly - app.listen port, host, (error) -> - logger.info "CLSI starting up, listening on #{host}:#{port}" + loadTcpServer.listen(load_tcp_port, host, function(error) { + if (error != null) { throw error; } + return logger.info(`Load tcp agent listening on load port ${load_tcp_port}`); + }); - loadTcpServer.listen load_tcp_port, host, (error) -> - throw error if error? - logger.info "Load tcp agent listening on load port #{load_tcp_port}" + loadHttpServer.listen(load_http_port, host, function(error) { + if (error != null) { throw error; } + return logger.info(`Load http agent listening on load port ${load_http_port}`); + }); +} - loadHttpServer.listen load_http_port, host, (error) -> - throw error if error? - logger.info "Load http agent listening on load port #{load_http_port}" +module.exports = app; -module.exports = app +setInterval(() => ProjectPersistenceManager.clearExpiredProjects() +, (tenMinutes = 10 * 60 * 1000)); -setInterval () -> - ProjectPersistenceManager.clearExpiredProjects() -, tenMinutes = 10 * 60 * 1000 +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/config/settings.defaults.js b/config/settings.defaults.js index ad3f04d8..5d0bb75f 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -1,71 +1,89 @@ -Path = require "path" +const Path = require("path"); -module.exports = - # Options are passed to Sequelize. - # See http://sequelizejs.com/documentation#usage-options for details - mysql: - clsi: - database: "clsi" - username: "clsi" - dialect: "sqlite" - storage: process.env["SQLITE_PATH"] or Path.resolve(__dirname + "/../db.sqlite") - pool: - max: 1 +module.exports = { + // Options are passed to Sequelize. + // See http://sequelizejs.com/documentation#usage-options for details + mysql: { + clsi: { + database: "clsi", + username: "clsi", + dialect: "sqlite", + storage: process.env["SQLITE_PATH"] || Path.resolve(__dirname + "/../db.sqlite"), + pool: { + max: 1, min: 1 - retry: + }, + retry: { max: 10 + } + } + }, - compileSizeLimit: process.env["COMPILE_SIZE_LIMIT"] or "7mb" + compileSizeLimit: process.env["COMPILE_SIZE_LIMIT"] || "7mb", - path: - compilesDir: Path.resolve(__dirname + "/../compiles") - clsiCacheDir: Path.resolve(__dirname + "/../cache") - synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) + path: { + compilesDir: Path.resolve(__dirname + "/../compiles"), + clsiCacheDir: Path.resolve(__dirname + "/../cache"), + synctexBaseDir(project_id) { return Path.join(this.compilesDir, project_id); } + }, - internal: - clsi: - port: 3013 - host: process.env["LISTEN_ADDRESS"] or "localhost" + internal: { + clsi: { + port: 3013, + host: process.env["LISTEN_ADDRESS"] || "localhost" + }, - load_balancer_agent: - report_load:true - load_port: 3048 + load_balancer_agent: { + report_load:true, + load_port: 3048, local_port: 3049 - apis: - clsi: - url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013" + } + }, + apis: { + clsi: { + url: `http://${process.env['CLSI_HOST'] || 'localhost'}:3013` + } + }, - smokeTest: process.env["SMOKE_TEST"] or false - project_cache_length_ms: 1000 * 60 * 60 * 24 - parallelFileDownloads: process.env["FILESTORE_PARALLEL_FILE_DOWNLOADS"] or 1 - parallelSqlQueryLimit: process.env["FILESTORE_PARALLEL_SQL_QUERY_LIMIT"] or 1 - filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"] - texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"] - sentry: + smokeTest: process.env["SMOKE_TEST"] || false, + project_cache_length_ms: 1000 * 60 * 60 * 24, + parallelFileDownloads: process.env["FILESTORE_PARALLEL_FILE_DOWNLOADS"] || 1, + parallelSqlQueryLimit: process.env["FILESTORE_PARALLEL_SQL_QUERY_LIMIT"] || 1, + filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"], + texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"], + sentry: { dsn: process.env['SENTRY_DSN'] + } +}; -if process.env["DOCKER_RUNNER"] - module.exports.clsi = - dockerRunner: process.env["DOCKER_RUNNER"] == "true" - docker: - image: process.env["TEXLIVE_IMAGE"] or "quay.io/sharelatex/texlive-full:2017.1" - env: +if (process.env["DOCKER_RUNNER"]) { + let seccomp_profile_path; + module.exports.clsi = { + dockerRunner: process.env["DOCKER_RUNNER"] === "true", + docker: { + image: process.env["TEXLIVE_IMAGE"] || "quay.io/sharelatex/texlive-full:2017.1", + env: { HOME: "/tmp" - socketPath: "/var/run/docker.sock" - user: process.env["TEXLIVE_IMAGE_USER"] or "tex" - expireProjectAfterIdleMs: 24 * 60 * 60 * 1000 + }, + socketPath: "/var/run/docker.sock", + user: process.env["TEXLIVE_IMAGE_USER"] || "tex" + }, + expireProjectAfterIdleMs: 24 * 60 * 60 * 1000, checkProjectsIntervalMs: 10 * 60 * 1000 + }; - try - seccomp_profile_path = Path.resolve(__dirname + "/../seccomp/clsi-profile.json") - module.exports.clsi.docker.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path))) - catch error - console.log error, "could not load seccom profile from #{seccomp_profile_path}" + try { + seccomp_profile_path = Path.resolve(__dirname + "/../seccomp/clsi-profile.json"); + module.exports.clsi.docker.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path))); + } catch (error) { + console.log(error, `could not load seccom profile from ${seccomp_profile_path}`); + } - module.exports.path.synctexBaseDir = -> "/compile" + module.exports.path.synctexBaseDir = () => "/compile"; - module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] + module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"]; - module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"] + module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"]; +} From de36ab663c3152cd33689130ae711e05a9c3cdb7 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:16:18 +0100 Subject: [PATCH 487/709] prettier: convert individual decaffeinated files to Prettier format --- app.js | 571 ++++++++++++++++++++---------------- config/settings.defaults.js | 167 ++++++----- 2 files changed, 411 insertions(+), 327 deletions(-) diff --git a/app.js b/app.js index 99427da2..c03fcd8b 100644 --- a/app.js +++ b/app.js @@ -5,294 +5,367 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let tenMinutes; -const Metrics = require("metrics-sharelatex"); -Metrics.initialize("clsi"); - -const CompileController = require("./app/js/CompileController"); -const Settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); -logger.initialize("clsi"); +let tenMinutes +const Metrics = require('metrics-sharelatex') +Metrics.initialize('clsi') + +const CompileController = require('./app/js/CompileController') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +logger.initialize('clsi') if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { - logger.initializeErrorReporting(Settings.sentry.dsn); + logger.initializeErrorReporting(Settings.sentry.dsn) } -const smokeTest = require("smoke-test-sharelatex"); -const ContentTypeMapper = require("./app/js/ContentTypeMapper"); -const Errors = require('./app/js/Errors'); +const smokeTest = require('smoke-test-sharelatex') +const ContentTypeMapper = require('./app/js/ContentTypeMapper') +const Errors = require('./app/js/Errors') -const Path = require("path"); -const fs = require("fs"); +const Path = require('path') +const fs = require('fs') +Metrics.open_sockets.monitor(logger) +Metrics.memory.monitor(logger) -Metrics.open_sockets.monitor(logger); -Metrics.memory.monitor(logger); +const ProjectPersistenceManager = require('./app/js/ProjectPersistenceManager') +const OutputCacheManager = require('./app/js/OutputCacheManager') -const ProjectPersistenceManager = require("./app/js/ProjectPersistenceManager"); -const OutputCacheManager = require("./app/js/OutputCacheManager"); +require('./app/js/db').sync() -require("./app/js/db").sync(); +const express = require('express') +const bodyParser = require('body-parser') +const app = express() -const express = require("express"); -const bodyParser = require("body-parser"); -const app = express(); - -Metrics.injectMetricsRoute(app); -app.use(Metrics.http.monitor(logger)); +Metrics.injectMetricsRoute(app) +app.use(Metrics.http.monitor(logger)) // Compile requests can take longer than the default two -// minutes (including file download time), so bump up the +// minutes (including file download time), so bump up the // timeout a bit. -const TIMEOUT = 10 * 60 * 1000; +const TIMEOUT = 10 * 60 * 1000 app.use(function(req, res, next) { - req.setTimeout(TIMEOUT); - res.setTimeout(TIMEOUT); - res.removeHeader("X-Powered-By"); - return next(); -}); + req.setTimeout(TIMEOUT) + res.setTimeout(TIMEOUT) + res.removeHeader('X-Powered-By') + return next() +}) app.param('project_id', function(req, res, next, project_id) { - if ((project_id != null ? project_id.match(/^[a-zA-Z0-9_-]+$/) : undefined)) { - return next(); - } else { - return next(new Error("invalid project id")); - } -}); + if (project_id != null ? project_id.match(/^[a-zA-Z0-9_-]+$/) : undefined) { + return next() + } else { + return next(new Error('invalid project id')) + } +}) app.param('user_id', function(req, res, next, user_id) { - if ((user_id != null ? user_id.match(/^[0-9a-f]{24}$/) : undefined)) { - return next(); - } else { - return next(new Error("invalid user id")); - } -}); + if (user_id != null ? user_id.match(/^[0-9a-f]{24}$/) : undefined) { + return next() + } else { + return next(new Error('invalid user id')) + } +}) app.param('build_id', function(req, res, next, build_id) { - if ((build_id != null ? build_id.match(OutputCacheManager.BUILD_REGEX) : undefined)) { - return next(); - } else { - return next(new Error(`invalid build id ${build_id}`)); - } -}); - - -app.post("/project/:project_id/compile", bodyParser.json({limit: Settings.compileSizeLimit}), 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); -app.get("/project/:project_id/sync/pdf", CompileController.syncFromPdf); -app.get("/project/:project_id/wordcount", CompileController.wordcount); -app.get("/project/:project_id/status", CompileController.status); + if ( + build_id != null + ? build_id.match(OutputCacheManager.BUILD_REGEX) + : undefined + ) { + return next() + } else { + return next(new Error(`invalid build id ${build_id}`)) + } +}) + +app.post( + '/project/:project_id/compile', + bodyParser.json({ limit: Settings.compileSizeLimit }), + 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) +app.get('/project/:project_id/sync/pdf', CompileController.syncFromPdf) +app.get('/project/:project_id/wordcount', CompileController.wordcount) +app.get('/project/:project_id/status', CompileController.status) // Per-user containers -app.post("/project/:project_id/user/:user_id/compile", bodyParser.json({limit: Settings.compileSizeLimit}), 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); -app.get("/project/:project_id/user/:user_id/sync/pdf", CompileController.syncFromPdf); -app.get("/project/:project_id/user/:user_id/wordcount", CompileController.wordcount); - -const ForbidSymlinks = require("./app/js/StaticServerForbidSymlinks"); +app.post( + '/project/:project_id/user/:user_id/compile', + bodyParser.json({ limit: Settings.compileSizeLimit }), + 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 +) +app.get( + '/project/:project_id/user/:user_id/sync/pdf', + CompileController.syncFromPdf +) +app.get( + '/project/:project_id/user/:user_id/wordcount', + CompileController.wordcount +) + +const ForbidSymlinks = require('./app/js/StaticServerForbidSymlinks') // create a static server which does not allow access to any symlinks // avoids possible mismatch of root directory between middleware check // and serving the files -const staticServer = ForbidSymlinks(express.static, Settings.path.compilesDir, { setHeaders(res, path, stat) { - if (Path.basename(path) === "output.pdf") { - // Calculate an etag in the same way as nginx - // https://github.com/tj/send/issues/65 - const etag = (path, stat) => - `"${Math.ceil(+stat.mtime / 1000).toString(16)}` + - '-' + Number(stat.size).toString(16) + '"' - ; - res.set("Etag", etag(path, stat)); - } - return res.set("Content-Type", ContentTypeMapper.map(path)); -} -} -); - -app.get("/project/:project_id/user/:user_id/build/:build_id/output/*", function(req, res, next) { - // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = `/${req.params.project_id}-${req.params.user_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`); - return staticServer(req, res, next); -}); - -app.get("/project/:project_id/build/:build_id/output/*", function(req, res, next) { - // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = `/${req.params.project_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`); - return staticServer(req, res, next); -}); - -app.get("/project/:project_id/user/:user_id/output/*", function(req, res, next) { - // for specific user get the path to the top level file - req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}`; - return staticServer(req, res, next); -}); - -app.get("/project/:project_id/output/*", function(req, res, next) { - if (((req.query != null ? req.query.build : undefined) != null) && req.query.build.match(OutputCacheManager.BUILD_REGEX)) { - // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = `/${req.params.project_id}/` + OutputCacheManager.path(req.query.build, `/${req.params[0]}`); - } else { - req.url = `/${req.params.project_id}/${req.params[0]}`; - } - return staticServer(req, res, next); -}); - -app.get("/oops", function(req, res, next) { - logger.error({err: "hello"}, "test error"); - return res.send("error\n"); -}); - - -app.get("/status", (req, res, next) => res.send("CLSI is alive\n")); +const staticServer = ForbidSymlinks(express.static, Settings.path.compilesDir, { + setHeaders(res, path, stat) { + if (Path.basename(path) === 'output.pdf') { + // Calculate an etag in the same way as nginx + // https://github.com/tj/send/issues/65 + const etag = (path, stat) => + `"${Math.ceil(+stat.mtime / 1000).toString(16)}` + + '-' + + Number(stat.size).toString(16) + + '"' + res.set('Etag', etag(path, stat)) + } + return res.set('Content-Type', ContentTypeMapper.map(path)) + } +}) + +app.get('/project/:project_id/user/:user_id/build/:build_id/output/*', function( + req, + res, + next +) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = + `/${req.params.project_id}-${req.params.user_id}/` + + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) + return staticServer(req, res, next) +}) + +app.get('/project/:project_id/build/:build_id/output/*', function( + req, + res, + next +) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = + `/${req.params.project_id}/` + + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) + return staticServer(req, res, next) +}) + +app.get('/project/:project_id/user/:user_id/output/*', function( + req, + res, + next +) { + // for specific user get the path to the top level file + req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}` + return staticServer(req, res, next) +}) + +app.get('/project/:project_id/output/*', function(req, res, next) { + if ( + (req.query != null ? req.query.build : undefined) != null && + req.query.build.match(OutputCacheManager.BUILD_REGEX) + ) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = + `/${req.params.project_id}/` + + OutputCacheManager.path(req.query.build, `/${req.params[0]}`) + } else { + req.url = `/${req.params.project_id}/${req.params[0]}` + } + return staticServer(req, res, next) +}) + +app.get('/oops', function(req, res, next) { + logger.error({ err: 'hello' }, 'test error') + return res.send('error\n') +}) + +app.get('/status', (req, res, next) => res.send('CLSI is alive\n')) const resCacher = { - contentType(setContentType){ - this.setContentType = setContentType; - }, - send(code, body){ - this.code = code; - this.body = body; - }, - - //default the server to be down - code:500, - body:{}, - setContentType:"application/json" -}; + contentType(setContentType) { + this.setContentType = setContentType + }, + send(code, body) { + this.code = code + this.body = body + }, + + // default the server to be down + code: 500, + body: {}, + setContentType: 'application/json' +} if (Settings.smokeTest) { - let runSmokeTest; - (runSmokeTest = function() { - logger.log("running smoke tests"); - smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher); - return setTimeout(runSmokeTest, 30 * 1000); - })(); + let runSmokeTest + ;(runSmokeTest = function() { + logger.log('running smoke tests') + smokeTest.run(require.resolve(__dirname + '/test/smoke/js/SmokeTests.js'))( + {}, + resCacher + ) + return setTimeout(runSmokeTest, 30 * 1000) + })() } -app.get("/health_check", function(req, res){ - res.contentType(resCacher != null ? resCacher.setContentType : undefined); - return res.status(resCacher != null ? resCacher.code : undefined).send(resCacher != null ? resCacher.body : undefined); -}); - -app.get("/smoke_test_force", (req, res)=> smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))(req, res)); - -const profiler = require("v8-profiler-node8"); -app.get("/profile", function(req, res) { - const time = parseInt(req.query.time || "1000"); - profiler.startProfiling("test"); - return setTimeout(function() { - const profile = profiler.stopProfiling("test"); - return res.json(profile); - } - , time); -}); - -app.get("/heapdump", (req, res)=> - require('heapdump').writeSnapshot(`/tmp/${Date.now()}.clsi.heapsnapshot`, (err, filename)=> res.send(filename)) -); +app.get('/health_check', function(req, res) { + res.contentType(resCacher != null ? resCacher.setContentType : undefined) + return res + .status(resCacher != null ? resCacher.code : undefined) + .send(resCacher != null ? resCacher.body : undefined) +}) + +app.get('/smoke_test_force', (req, res) => + smokeTest.run(require.resolve(__dirname + '/test/smoke/js/SmokeTests.js'))( + req, + res + ) +) + +const profiler = require('v8-profiler-node8') +app.get('/profile', function(req, res) { + const time = parseInt(req.query.time || '1000') + profiler.startProfiling('test') + return setTimeout(function() { + const profile = profiler.stopProfiling('test') + return res.json(profile) + }, time) +}) + +app.get('/heapdump', (req, res) => + require('heapdump').writeSnapshot( + `/tmp/${Date.now()}.clsi.heapsnapshot`, + (err, filename) => res.send(filename) + ) +) app.use(function(error, req, res, next) { - if (error instanceof Errors.NotFoundError) { - logger.warn({err: error, url: req.url}, "not found error"); - return res.sendStatus(404); - } else { - logger.error({err: error, url: req.url}, "server error"); - return res.sendStatus((error != null ? error.statusCode : undefined) || 500); - } -}); + if (error instanceof Errors.NotFoundError) { + logger.warn({ err: error, url: req.url }, 'not found error') + return res.sendStatus(404) + } else { + logger.error({ err: error, url: req.url }, 'server error') + return res.sendStatus((error != null ? error.statusCode : undefined) || 500) + } +}) -const net = require("net"); -const os = require("os"); - -let STATE = "up"; +const net = require('net') +const os = require('os') +let STATE = 'up' const loadTcpServer = net.createServer(function(socket) { - socket.on("error", function(err){ - if (err.code === "ECONNRESET") { - // this always comes up, we don't know why - return; - } - logger.err({err}, "error with socket on load check"); - return socket.destroy(); - }); - - if ((STATE === "up") && Settings.internal.load_balancer_agent.report_load) { - let availableWorkingCpus; - const currentLoad = os.loadavg()[0]; - - // staging clis's have 1 cpu core only - if (os.cpus().length === 1) { - availableWorkingCpus = 1; - } else { - availableWorkingCpus = os.cpus().length - 1; - } - - const freeLoad = availableWorkingCpus - currentLoad; - let freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100); - if (freeLoadPercentage <= 0) { - freeLoadPercentage = 1; // when its 0 the server is set to drain and will move projects to different servers - } - socket.write(`up, ${freeLoadPercentage}%\n`, "ASCII"); - return socket.end(); - } else { - socket.write(`${STATE}\n`, "ASCII"); - return socket.end(); - } -}); - -const loadHttpServer = express(); - -loadHttpServer.post("/state/up", function(req, res, next) { - STATE = "up"; - logger.info("getting message to set server to down"); - return res.sendStatus(204); -}); - -loadHttpServer.post("/state/down", function(req, res, next) { - STATE = "down"; - logger.info("getting message to set server to down"); - return res.sendStatus(204); -}); - -loadHttpServer.post("/state/maint", function(req, res, next) { - STATE = "maint"; - logger.info("getting message to set server to maint"); - return res.sendStatus(204); -}); - - -const port = (__guard__(Settings.internal != null ? Settings.internal.clsi : undefined, x => x.port) || 3013); -const host = (__guard__(Settings.internal != null ? Settings.internal.clsi : undefined, x1 => x1.host) || "localhost"); - -const load_tcp_port = Settings.internal.load_balancer_agent.load_port; -const load_http_port = Settings.internal.load_balancer_agent.local_port; - -if (!module.parent) { // Called directly - app.listen(port, host, error => logger.info(`CLSI starting up, listening on ${host}:${port}`)); - - loadTcpServer.listen(load_tcp_port, host, function(error) { - if (error != null) { throw error; } - return logger.info(`Load tcp agent listening on load port ${load_tcp_port}`); - }); - - loadHttpServer.listen(load_http_port, host, function(error) { - if (error != null) { throw error; } - return logger.info(`Load http agent listening on load port ${load_http_port}`); - }); + socket.on('error', function(err) { + if (err.code === 'ECONNRESET') { + // this always comes up, we don't know why + return + } + logger.err({ err }, 'error with socket on load check') + return socket.destroy() + }) + + if (STATE === 'up' && Settings.internal.load_balancer_agent.report_load) { + let availableWorkingCpus + const currentLoad = os.loadavg()[0] + + // staging clis's have 1 cpu core only + if (os.cpus().length === 1) { + availableWorkingCpus = 1 + } else { + availableWorkingCpus = os.cpus().length - 1 + } + + const freeLoad = availableWorkingCpus - currentLoad + let freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) + if (freeLoadPercentage <= 0) { + freeLoadPercentage = 1 // when its 0 the server is set to drain and will move projects to different servers + } + socket.write(`up, ${freeLoadPercentage}%\n`, 'ASCII') + return socket.end() + } else { + socket.write(`${STATE}\n`, 'ASCII') + return socket.end() + } +}) + +const loadHttpServer = express() + +loadHttpServer.post('/state/up', function(req, res, next) { + STATE = 'up' + logger.info('getting message to set server to down') + return res.sendStatus(204) +}) + +loadHttpServer.post('/state/down', function(req, res, next) { + STATE = 'down' + logger.info('getting message to set server to down') + return res.sendStatus(204) +}) + +loadHttpServer.post('/state/maint', function(req, res, next) { + STATE = 'maint' + logger.info('getting message to set server to maint') + return res.sendStatus(204) +}) + +const port = + __guard__( + Settings.internal != null ? Settings.internal.clsi : undefined, + x => x.port + ) || 3013 +const host = + __guard__( + Settings.internal != null ? Settings.internal.clsi : undefined, + x1 => x1.host + ) || 'localhost' + +const load_tcp_port = Settings.internal.load_balancer_agent.load_port +const load_http_port = Settings.internal.load_balancer_agent.local_port + +if (!module.parent) { + // Called directly + app.listen(port, host, error => + logger.info(`CLSI starting up, listening on ${host}:${port}`) + ) + + loadTcpServer.listen(load_tcp_port, host, function(error) { + if (error != null) { + throw error + } + return logger.info(`Load tcp agent listening on load port ${load_tcp_port}`) + }) + + loadHttpServer.listen(load_http_port, host, function(error) { + if (error != null) { + throw error + } + return logger.info( + `Load http agent listening on load port ${load_http_port}` + ) + }) } -module.exports = app; - -setInterval(() => ProjectPersistenceManager.clearExpiredProjects() -, (tenMinutes = 10 * 60 * 1000)); +module.exports = app +setInterval( + () => ProjectPersistenceManager.clearExpiredProjects(), + (tenMinutes = 10 * 60 * 1000) +) function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 5d0bb75f..b0fd0cbd 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -1,89 +1,100 @@ -const Path = require("path"); +const Path = require('path') module.exports = { - // Options are passed to Sequelize. - // See http://sequelizejs.com/documentation#usage-options for details - mysql: { - clsi: { - database: "clsi", - username: "clsi", - dialect: "sqlite", - storage: process.env["SQLITE_PATH"] || Path.resolve(__dirname + "/../db.sqlite"), - pool: { - max: 1, - min: 1 - }, - retry: { - max: 10 - } - } - }, + // Options are passed to Sequelize. + // See http://sequelizejs.com/documentation#usage-options for details + mysql: { + clsi: { + database: 'clsi', + username: 'clsi', + dialect: 'sqlite', + storage: + process.env.SQLITE_PATH || Path.resolve(__dirname + '/../db.sqlite'), + pool: { + max: 1, + min: 1 + }, + retry: { + max: 10 + } + } + }, - compileSizeLimit: process.env["COMPILE_SIZE_LIMIT"] || "7mb", - - path: { - compilesDir: Path.resolve(__dirname + "/../compiles"), - clsiCacheDir: Path.resolve(__dirname + "/../cache"), - synctexBaseDir(project_id) { return Path.join(this.compilesDir, project_id); } - }, + compileSizeLimit: process.env.COMPILE_SIZE_LIMIT || '7mb', - internal: { - clsi: { - port: 3013, - host: process.env["LISTEN_ADDRESS"] || "localhost" - }, - - load_balancer_agent: { - report_load:true, - load_port: 3048, - local_port: 3049 - } - }, - apis: { - clsi: { - url: `http://${process.env['CLSI_HOST'] || 'localhost'}:3013` - } - }, + path: { + compilesDir: Path.resolve(__dirname + '/../compiles'), + clsiCacheDir: Path.resolve(__dirname + '/../cache'), + synctexBaseDir(project_id) { + return Path.join(this.compilesDir, project_id) + } + }, - - smokeTest: process.env["SMOKE_TEST"] || false, - project_cache_length_ms: 1000 * 60 * 60 * 24, - parallelFileDownloads: process.env["FILESTORE_PARALLEL_FILE_DOWNLOADS"] || 1, - parallelSqlQueryLimit: process.env["FILESTORE_PARALLEL_SQL_QUERY_LIMIT"] || 1, - filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"], - texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"], - sentry: { - dsn: process.env['SENTRY_DSN'] - } -}; + internal: { + clsi: { + port: 3013, + host: process.env.LISTEN_ADDRESS || 'localhost' + }, + load_balancer_agent: { + report_load: true, + load_port: 3048, + local_port: 3049 + } + }, + apis: { + clsi: { + url: `http://${process.env.CLSI_HOST || 'localhost'}:3013` + } + }, -if (process.env["DOCKER_RUNNER"]) { - let seccomp_profile_path; - module.exports.clsi = { - dockerRunner: process.env["DOCKER_RUNNER"] === "true", - docker: { - image: process.env["TEXLIVE_IMAGE"] || "quay.io/sharelatex/texlive-full:2017.1", - env: { - HOME: "/tmp" - }, - socketPath: "/var/run/docker.sock", - user: process.env["TEXLIVE_IMAGE_USER"] || "tex" - }, - expireProjectAfterIdleMs: 24 * 60 * 60 * 1000, - checkProjectsIntervalMs: 10 * 60 * 1000 - }; + smokeTest: process.env.SMOKE_TEST || false, + project_cache_length_ms: 1000 * 60 * 60 * 24, + parallelFileDownloads: process.env.FILESTORE_PARALLEL_FILE_DOWNLOADS || 1, + parallelSqlQueryLimit: process.env.FILESTORE_PARALLEL_SQL_QUERY_LIMIT || 1, + filestoreDomainOveride: process.env.FILESTORE_DOMAIN_OVERRIDE, + texliveImageNameOveride: process.env.TEX_LIVE_IMAGE_NAME_OVERRIDE, + sentry: { + dsn: process.env.SENTRY_DSN + } +} + +if (process.env.DOCKER_RUNNER) { + let seccomp_profile_path + module.exports.clsi = { + dockerRunner: process.env.DOCKER_RUNNER === 'true', + docker: { + image: + process.env.TEXLIVE_IMAGE || + 'quay.io/sharelatex/texlive-full:2017.1', + env: { + HOME: '/tmp' + }, + socketPath: '/var/run/docker.sock', + user: process.env.TEXLIVE_IMAGE_USER || 'tex' + }, + expireProjectAfterIdleMs: 24 * 60 * 60 * 1000, + checkProjectsIntervalMs: 10 * 60 * 1000 + } + + try { + seccomp_profile_path = Path.resolve( + __dirname + '/../seccomp/clsi-profile.json' + ) + module.exports.clsi.docker.seccomp_profile = JSON.stringify( + JSON.parse(require('fs').readFileSync(seccomp_profile_path)) + ) + } catch (error) { + console.log( + error, + `could not load seccom profile from ${seccomp_profile_path}` + ) + } - try { - seccomp_profile_path = Path.resolve(__dirname + "/../seccomp/clsi-profile.json"); - module.exports.clsi.docker.seccomp_profile = JSON.stringify(JSON.parse(require("fs").readFileSync(seccomp_profile_path))); - } catch (error) { - console.log(error, `could not load seccom profile from ${seccomp_profile_path}`); - } + module.exports.path.synctexBaseDir = () => '/compile' - module.exports.path.synctexBaseDir = () => "/compile"; - - module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"]; + module.exports.path.sandboxedCompilesHostDir = + process.env.COMPILES_HOST_DIR - module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"]; + module.exports.path.synctexBinHostPath = process.env.SYNCTEX_BIN_HOST_PATH } From 7e737bba4f9c2c4bca1aebb14d238dfeac06c1db Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 19 Feb 2020 12:38:54 +0100 Subject: [PATCH 488/709] fixed test paths --- test/unit/js/DockerLockManagerTests.js | 2 +- test/unit/js/DockerRunnerTests.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/js/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js index 9dcf9dcc..177d7a21 100644 --- a/test/unit/js/DockerLockManagerTests.js +++ b/test/unit/js/DockerLockManagerTests.js @@ -15,7 +15,7 @@ require('chai').should() require('coffee-script') const modulePath = require('path').join( __dirname, - '../../../app/coffee/DockerLockManager' + '../../../app/js/DockerLockManager' ) describe('LockManager', function() { diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index e43a044c..597c5d30 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -20,7 +20,7 @@ const { expect } = require('chai') require('coffee-script') const modulePath = require('path').join( __dirname, - '../../../app/coffee/DockerRunner' + '../../../app/js/DockerRunner' ) const Path = require('path') From ee12573b0601b031bf5bac9e7cdb6d6b717a5f99 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Thu, 20 Feb 2020 17:24:28 +0100 Subject: [PATCH 489/709] added container monitor cleanup to fix hanging tests --- app/js/DockerRunner.js | 31 +++++++++++++++++++------- npm-shrinkwrap.json | 6 ----- package.json | 1 - test/unit/js/DockerLockManagerTests.js | 1 - test/unit/js/DockerRunnerTests.js | 5 ++++- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 5ac234b7..393ce3df 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -35,6 +35,9 @@ const usingSiblingContainers = () => x => x.sandboxedCompilesHostDir ) != null +let containerMonitorTimeout +let containerMonitorInterval + module.exports = DockerRunner = { ERR_NOT_DIRECTORY: new Error('not a directory'), ERR_TERMINATED: new Error('terminated'), @@ -646,17 +649,29 @@ module.exports = DockerRunner = { { maxAge: DockerRunner.MAX_CONTAINER_AGE }, 'starting container expiry' ) + + // guarantee only one monitor is running + DockerRunner.stopContainerMonitor() + // randomise the start time const randomDelay = Math.floor(Math.random() * 5 * 60 * 1000) - return setTimeout( - () => - setInterval( - () => DockerRunner.destroyOldContainers(), - (oneHour = 60 * 60 * 1000) - ), + containerMonitorTimeout = setTimeout(() => { + containerMonitorInterval = setInterval( + () => DockerRunner.destroyOldContainers(), + (oneHour = 60 * 60 * 1000) + ) + }, randomDelay) + }, - randomDelay - ) + stopContainerMonitor() { + if (containerMonitorTimeout) { + clearTimeout(containerMonitorTimeout) + containerMonitorTimeout = undefined + } + if (containerMonitorInterval) { + clearInterval(containerMonitorTimeout) + containerMonitorTimeout = undefined + } } } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 40fed3c2..05e00cd7 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1536,12 +1536,6 @@ "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" }, - "coffeescript": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.6.0.tgz", - "integrity": "sha1-bdTeHrYveE2MjYCWdVLLpUf/2d4=", - "dev": true - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", diff --git a/package.json b/package.json index c38621aa..87403ca5 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "babel-eslint": "^10.0.3", "bunyan": "^0.22.1", "chai": "~1.8.1", - "coffeescript": "1.6.0", "eslint": "^6.6.0", "eslint-config-prettier": "^6.10.0", "eslint-config-standard": "^14.1.0", diff --git a/test/unit/js/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js index 177d7a21..bc13c5ad 100644 --- a/test/unit/js/DockerLockManagerTests.js +++ b/test/unit/js/DockerLockManagerTests.js @@ -12,7 +12,6 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') require('chai').should() -require('coffee-script') const modulePath = require('path').join( __dirname, '../../../app/js/DockerLockManager' diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 597c5d30..d17d906d 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -17,7 +17,6 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') require('chai').should() const { expect } = require('chai') -require('coffee-script') const modulePath = require('path').join( __dirname, '../../../app/js/DockerRunner' @@ -89,6 +88,10 @@ describe('DockerRunner', function() { return (this.Settings.clsi.docker.env = { PATH: 'mock-path' }) }) + afterEach(function() { + this.DockerRunner.stopContainerMonitor() + }) + describe('run', function() { beforeEach(function(done) { this.DockerRunner._getContainerOptions = sinon From 8bcbffccdc6ffa60991245a410e4cbf48be108eb Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Wed, 12 Feb 2020 14:39:54 +0100 Subject: [PATCH 490/709] [misc] rename npm-shrinkwrap.json to package-lock.json --- npm-shrinkwrap.json => package-lock.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename npm-shrinkwrap.json => package-lock.json (100%) diff --git a/npm-shrinkwrap.json b/package-lock.json similarity index 100% rename from npm-shrinkwrap.json rename to package-lock.json From 8772e1f7b18fe2933a2f4b9dec9253cede66ed7c Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Wed, 12 Feb 2020 14:44:16 +0100 Subject: [PATCH 491/709] [misc] cleanup unused dependency on mongo and redis --- buildscript.txt | 2 +- docker-compose.ci.yml | 10 ---------- docker-compose.yml | 11 ----------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/buildscript.txt b/buildscript.txt index 9cccf33a..8db0cd0e 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -4,7 +4,7 @@ clsi --env-add= --node-version=10.19.0 --acceptance-creds=None ---dependencies=mongo,redis +--dependencies= --docker-repos=gcr.io/overleaf-ops --env-pass-through=TEXLIVE_IMAGE --script-version=1.3.5 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index d80a4ce1..aa7243e1 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -27,11 +27,6 @@ services: MOCHA_GREP: ${MOCHA_GREP} NODE_ENV: test TEXLIVE_IMAGE: - depends_on: - mongo: - condition: service_healthy - redis: - condition: service_healthy command: npm run test:acceptance:_run @@ -42,8 +37,3 @@ services: - ./:/tmp/build/ command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . user: root - redis: - image: redis - - mongo: - image: mongo:3.6 diff --git a/docker-compose.yml b/docker-compose.yml index 8cc59733..74b4471e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,16 +36,5 @@ services: MOCHA_GREP: ${MOCHA_GREP} LOG_LEVEL: ERROR NODE_ENV: test - depends_on: - mongo: - condition: service_healthy - redis: - condition: service_healthy command: npm run test:acceptance - redis: - image: redis - - mongo: - image: mongo:3.6 - From d1e0b8548eb86a9935f29d5d1dc4355acff5a1d7 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Tue, 25 Feb 2020 17:03:36 +0100 Subject: [PATCH 492/709] decaffeinate: Rename SmokeTests.coffee from .coffee to .js --- test/smoke/coffee/{SmokeTests.coffee => SmokeTests.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/smoke/coffee/{SmokeTests.coffee => SmokeTests.js} (100%) diff --git a/test/smoke/coffee/SmokeTests.coffee b/test/smoke/coffee/SmokeTests.js similarity index 100% rename from test/smoke/coffee/SmokeTests.coffee rename to test/smoke/coffee/SmokeTests.js From 4f01b7716e012b2104774eb65bcb6ca9a8bb6490 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Tue, 25 Feb 2020 17:03:37 +0100 Subject: [PATCH 493/709] decaffeinate: Convert SmokeTests.coffee to JS --- test/smoke/coffee/SmokeTests.js | 76 +++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/test/smoke/coffee/SmokeTests.js b/test/smoke/coffee/SmokeTests.js index 9ecf09c1..a8ad80b5 100644 --- a/test/smoke/coffee/SmokeTests.js +++ b/test/smoke/coffee/SmokeTests.js @@ -1,22 +1,29 @@ -chai = require("chai") -chai.should() unless Object.prototype.should? -expect = chai.expect -request = require "request" -Settings = require "settings-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require("chai"); +if (Object.prototype.should == null) { chai.should(); } +const { expect } = chai; +const request = require("request"); +const Settings = require("settings-sharelatex"); -buildUrl = (path) -> "http://#{Settings.internal.clsi.host}:#{Settings.internal.clsi.port}/#{path}" +const buildUrl = path => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}`; -url = buildUrl("project/smoketest-#{process.pid}/compile") +const url = buildUrl(`project/smoketest-${process.pid}/compile`); -describe "Running a compile", -> - before (done) -> - request.post { - url: url - json: - compile: - resources: [ - path: "main.tex" - content: """ +describe("Running a compile", function() { + before(function(done) { + return request.post({ + url, + json: { + compile: { + resources: [{ + path: "main.tex", + content: `\ % Membrane-like surface % Author: Yotam Avital \\documentclass{article} @@ -47,18 +54,31 @@ describe "Running a compile", -> } } \\end{tikzpicture} -\\end{document} - """ +\\end{document}\ +` + } ] - }, (@error, @response, @body) => - done() + } + } + }, (error, response, body) => { + this.error = error; + this.response = response; + this.body = body; + return done(); + }); + }); - it "should return the pdf", -> - for file in @body.compile.outputFiles - return if file.type == "pdf" - throw new Error("no pdf returned") + it("should return the pdf", function() { + for (let file of Array.from(this.body.compile.outputFiles)) { + if (file.type === "pdf") { return; } + } + throw new Error("no pdf returned"); + }); - it "should return the log", -> - for file in @body.compile.outputFiles - return if file.type == "log" - throw new Error("no log returned") + return it("should return the log", function() { + for (let file of Array.from(this.body.compile.outputFiles)) { + if (file.type === "log") { return; } + } + throw new Error("no log returned"); + }); +}); From 699000111bf281404774ee1f42cb6294a89d37bb Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Tue, 25 Feb 2020 17:03:39 +0100 Subject: [PATCH 494/709] decaffeinate: Run post-processing cleanups on SmokeTests.coffee --- test/smoke/coffee/SmokeTests.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/smoke/coffee/SmokeTests.js b/test/smoke/coffee/SmokeTests.js index a8ad80b5..6540401c 100644 --- a/test/smoke/coffee/SmokeTests.js +++ b/test/smoke/coffee/SmokeTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -69,14 +74,14 @@ describe("Running a compile", function() { }); it("should return the pdf", function() { - for (let file of Array.from(this.body.compile.outputFiles)) { + for (const file of Array.from(this.body.compile.outputFiles)) { if (file.type === "pdf") { return; } } throw new Error("no pdf returned"); }); return it("should return the log", function() { - for (let file of Array.from(this.body.compile.outputFiles)) { + for (const file of Array.from(this.body.compile.outputFiles)) { if (file.type === "log") { return; } } throw new Error("no log returned"); From d180fcd84a5183662398f2c8f80bc468bbcc6ef4 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Tue, 25 Feb 2020 17:03:57 +0100 Subject: [PATCH 495/709] decaffeinate: Rename loadTest.coffee from .coffee to .js --- test/load/coffee/{loadTest.coffee => loadTest.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/load/coffee/{loadTest.coffee => loadTest.js} (100%) diff --git a/test/load/coffee/loadTest.coffee b/test/load/coffee/loadTest.js similarity index 100% rename from test/load/coffee/loadTest.coffee rename to test/load/coffee/loadTest.js From a327c217e797656e941a8cd7300a5dbfa691e0cd Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Tue, 25 Feb 2020 17:03:57 +0100 Subject: [PATCH 496/709] decaffeinate: Convert loadTest.coffee to JS --- test/load/coffee/loadTest.js | 144 +++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 59 deletions(-) diff --git a/test/load/coffee/loadTest.js b/test/load/coffee/loadTest.js index 26a23fba..d5ddecf6 100644 --- a/test/load/coffee/loadTest.js +++ b/test/load/coffee/loadTest.js @@ -1,71 +1,97 @@ -request = require "request" -Settings = require "settings-sharelatex" -async = require("async") -fs = require("fs") -_ = require("underscore") -concurentCompiles = 5 -totalCompiles = 50 +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const request = require("request"); +const Settings = require("settings-sharelatex"); +const async = require("async"); +const fs = require("fs"); +const _ = require("underscore"); +const concurentCompiles = 5; +const totalCompiles = 50; -buildUrl = (path) -> "http://#{Settings.internal.clsi.host}:#{Settings.internal.clsi.port}/#{path}" +const buildUrl = path => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}`; -mainTexContent = fs.readFileSync("./bulk.tex", "utf-8") +const mainTexContent = fs.readFileSync("./bulk.tex", "utf-8"); -compileTimes = [] -failedCount = 0 +const compileTimes = []; +let failedCount = 0; -getAverageCompileTime = -> - totalTime = _.reduce compileTimes, (sum, time)-> - sum + time - , 0 - return totalTime / compileTimes.length +const getAverageCompileTime = function() { + const totalTime = _.reduce(compileTimes, (sum, time)=> sum + time + , 0); + return totalTime / compileTimes.length; +}; -makeRequest = (compileNumber, callback)-> - bulkBodyCount = 7 - bodyContent = "" - while --bulkBodyCount - bodyContent = bodyContent+=mainTexContent +const makeRequest = function(compileNumber, callback){ + let bulkBodyCount = 7; + let bodyContent = ""; + while (--bulkBodyCount) { + bodyContent = (bodyContent+=mainTexContent); + } - startTime = new Date() - request.post { - url: buildUrl("project/loadcompile-#{compileNumber}/compile") - json: - compile: - resources: [ - path: "main.tex" - content: """ - \\documentclass{article} - \\begin{document} - #{bodyContent} - \\end{document} - """ + const startTime = new Date(); + return request.post({ + url: buildUrl(`project/loadcompile-${compileNumber}/compile`), + json: { + compile: { + resources: [{ + path: "main.tex", + content: `\ +\\documentclass{article} +\\begin{document} +${bodyContent} +\\end{document}\ +` + } ] - }, (err, response, body)-> - if response.statusCode != 200 - failedCount++ - return callback("compile #{compileNumber} failed") - if err? - failedCount++ - return callback("failed") - totalTime = new Date() - startTime - console.log totalTime+"ms" - compileTimes.push(totalTime) - callback(err) + } + } + }, function(err, response, body){ + if (response.statusCode !== 200) { + failedCount++; + return callback(`compile ${compileNumber} failed`); + } + if (err != null) { + failedCount++; + return callback("failed"); + } + const totalTime = new Date() - startTime; + console.log(totalTime+"ms"); + compileTimes.push(totalTime); + return callback(err); + }); +}; -jobs = _.map [1..totalCompiles], (i)-> - return (cb)-> - makeRequest(i, cb) +const jobs = _.map(__range__(1, totalCompiles, true), i=> + cb=> makeRequest(i, cb) +); -startTime = new Date() -async.parallelLimit jobs, concurentCompiles, (err)-> - if err? - console.error err - console.log("total time taken = #{(new Date() - startTime)/1000}s") - console.log("total compiles = #{totalCompiles}") - console.log("concurent compiles = #{concurentCompiles}") - console.log("average time = #{getAverageCompileTime()/1000}s") - console.log("max time = #{_.max(compileTimes)/1000}s") - console.log("min time = #{_.min(compileTimes)/1000}s") - console.log("total failures = #{failedCount}") +const startTime = new Date(); +async.parallelLimit(jobs, concurentCompiles, function(err){ + if (err != null) { + console.error(err); + } + console.log(`total time taken = ${(new Date() - startTime)/1000}s`); + console.log(`total compiles = ${totalCompiles}`); + console.log(`concurent compiles = ${concurentCompiles}`); + console.log(`average time = ${getAverageCompileTime()/1000}s`); + console.log(`max time = ${_.max(compileTimes)/1000}s`); + console.log(`min time = ${_.min(compileTimes)/1000}s`); + return console.log(`total failures = ${failedCount}`); +}); + +function __range__(left, right, inclusive) { + let range = []; + let ascending = left < right; + let end = !inclusive ? right : ascending ? right + 1 : right - 1; + for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { + range.push(i); + } + return range; +} \ No newline at end of file From 957f80ada4a9518a2cd7dc0cfa5e56d6a75c2fd3 Mon Sep 17 00:00:00 2001 From: decaffeinate <mserranom@gmail.com> Date: Tue, 25 Feb 2020 17:03:58 +0100 Subject: [PATCH 497/709] decaffeinate: Run post-processing cleanups on loadTest.coffee --- test/load/coffee/loadTest.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/load/coffee/loadTest.js b/test/load/coffee/loadTest.js index d5ddecf6..2e769a59 100644 --- a/test/load/coffee/loadTest.js +++ b/test/load/coffee/loadTest.js @@ -1,3 +1,8 @@ +/* eslint-disable + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -50,7 +55,7 @@ ${bodyContent} ] } } - }, function(err, response, body){ + }, (err, response, body) => { if (response.statusCode !== 200) { failedCount++; return callback(`compile ${compileNumber} failed`); @@ -72,7 +77,7 @@ const jobs = _.map(__range__(1, totalCompiles, true), i=> ); const startTime = new Date(); -async.parallelLimit(jobs, concurentCompiles, function(err){ +async.parallelLimit(jobs, concurentCompiles, (err) => { if (err != null) { console.error(err); } @@ -87,9 +92,9 @@ async.parallelLimit(jobs, concurentCompiles, function(err){ function __range__(left, right, inclusive) { - let range = []; - let ascending = left < right; - let end = !inclusive ? right : ascending ? right + 1 : right - 1; + const range = []; + const ascending = left < right; + const end = !inclusive ? right : ascending ? right + 1 : right - 1; for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { range.push(i); } From bf470cf5ae149487ccf484d15c272f4d32174e4e Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Tue, 25 Feb 2020 17:15:31 +0100 Subject: [PATCH 498/709] moved decaffeinated files to js folder --- test/load/coffee/loadTest.js | 102 ----------------------- test/load/{coffee => js}/bulk.tex | 0 test/load/js/loadTest.js | 103 ++++++++++++++++++++++++ test/smoke/{coffee => js}/SmokeTests.js | 93 +++++++++++---------- 4 files changed, 155 insertions(+), 143 deletions(-) delete mode 100644 test/load/coffee/loadTest.js rename test/load/{coffee => js}/bulk.tex (100%) create mode 100644 test/load/js/loadTest.js rename test/smoke/{coffee => js}/SmokeTests.js (62%) diff --git a/test/load/coffee/loadTest.js b/test/load/coffee/loadTest.js deleted file mode 100644 index 2e769a59..00000000 --- a/test/load/coffee/loadTest.js +++ /dev/null @@ -1,102 +0,0 @@ -/* eslint-disable - standard/no-callback-literal, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const request = require("request"); -const Settings = require("settings-sharelatex"); -const async = require("async"); -const fs = require("fs"); -const _ = require("underscore"); -const concurentCompiles = 5; -const totalCompiles = 50; - -const buildUrl = path => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}`; - -const mainTexContent = fs.readFileSync("./bulk.tex", "utf-8"); - -const compileTimes = []; -let failedCount = 0; - -const getAverageCompileTime = function() { - const totalTime = _.reduce(compileTimes, (sum, time)=> sum + time - , 0); - return totalTime / compileTimes.length; -}; - -const makeRequest = function(compileNumber, callback){ - let bulkBodyCount = 7; - let bodyContent = ""; - while (--bulkBodyCount) { - bodyContent = (bodyContent+=mainTexContent); - } - - - const startTime = new Date(); - return request.post({ - url: buildUrl(`project/loadcompile-${compileNumber}/compile`), - json: { - compile: { - resources: [{ - path: "main.tex", - content: `\ -\\documentclass{article} -\\begin{document} -${bodyContent} -\\end{document}\ -` - } - ] - } - } - }, (err, response, body) => { - if (response.statusCode !== 200) { - failedCount++; - return callback(`compile ${compileNumber} failed`); - } - if (err != null) { - failedCount++; - return callback("failed"); - } - const totalTime = new Date() - startTime; - console.log(totalTime+"ms"); - compileTimes.push(totalTime); - return callback(err); - }); -}; - - -const jobs = _.map(__range__(1, totalCompiles, true), i=> - cb=> makeRequest(i, cb) -); - -const startTime = new Date(); -async.parallelLimit(jobs, concurentCompiles, (err) => { - if (err != null) { - console.error(err); - } - console.log(`total time taken = ${(new Date() - startTime)/1000}s`); - console.log(`total compiles = ${totalCompiles}`); - console.log(`concurent compiles = ${concurentCompiles}`); - console.log(`average time = ${getAverageCompileTime()/1000}s`); - console.log(`max time = ${_.max(compileTimes)/1000}s`); - console.log(`min time = ${_.min(compileTimes)/1000}s`); - return console.log(`total failures = ${failedCount}`); -}); - - -function __range__(left, right, inclusive) { - const range = []; - const ascending = left < right; - const end = !inclusive ? right : ascending ? right + 1 : right - 1; - for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { - range.push(i); - } - return range; -} \ No newline at end of file diff --git a/test/load/coffee/bulk.tex b/test/load/js/bulk.tex similarity index 100% rename from test/load/coffee/bulk.tex rename to test/load/js/bulk.tex diff --git a/test/load/js/loadTest.js b/test/load/js/loadTest.js new file mode 100644 index 00000000..ff9850ef --- /dev/null +++ b/test/load/js/loadTest.js @@ -0,0 +1,103 @@ +/* eslint-disable + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const request = require('request') +const Settings = require('settings-sharelatex') +const async = require('async') +const fs = require('fs') +const _ = require('underscore') +const concurentCompiles = 5 +const totalCompiles = 50 + +const buildUrl = path => + `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}` + +const mainTexContent = fs.readFileSync('./bulk.tex', 'utf-8') + +const compileTimes = [] +let failedCount = 0 + +const getAverageCompileTime = function() { + const totalTime = _.reduce(compileTimes, (sum, time) => sum + time, 0) + return totalTime / compileTimes.length +} + +const makeRequest = function(compileNumber, callback) { + let bulkBodyCount = 7 + let bodyContent = '' + while (--bulkBodyCount) { + bodyContent = bodyContent += mainTexContent + } + + const startTime = new Date() + return request.post( + { + url: buildUrl(`project/loadcompile-${compileNumber}/compile`), + json: { + compile: { + resources: [ + { + path: 'main.tex', + content: `\ +\\documentclass{article} +\\begin{document} +${bodyContent} +\\end{document}\ +` + } + ] + } + } + }, + (err, response, body) => { + if (response.statusCode !== 200) { + failedCount++ + return callback(`compile ${compileNumber} failed`) + } + if (err != null) { + failedCount++ + return callback('failed') + } + const totalTime = new Date() - startTime + console.log(totalTime + 'ms') + compileTimes.push(totalTime) + return callback(err) + } + ) +} + +const jobs = _.map(__range__(1, totalCompiles, true), i => cb => + makeRequest(i, cb) +) + +const startTime = new Date() +async.parallelLimit(jobs, concurentCompiles, err => { + if (err != null) { + console.error(err) + } + console.log(`total time taken = ${(new Date() - startTime) / 1000}s`) + console.log(`total compiles = ${totalCompiles}`) + console.log(`concurent compiles = ${concurentCompiles}`) + console.log(`average time = ${getAverageCompileTime() / 1000}s`) + console.log(`max time = ${_.max(compileTimes) / 1000}s`) + console.log(`min time = ${_.min(compileTimes) / 1000}s`) + return console.log(`total failures = ${failedCount}`) +}) + +function __range__(left, right, inclusive) { + const range = [] + const ascending = left < right + const end = !inclusive ? right : ascending ? right + 1 : right - 1 + for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { + range.push(i) + } + return range +} diff --git a/test/smoke/coffee/SmokeTests.js b/test/smoke/js/SmokeTests.js similarity index 62% rename from test/smoke/coffee/SmokeTests.js rename to test/smoke/js/SmokeTests.js index 6540401c..851ea850 100644 --- a/test/smoke/coffee/SmokeTests.js +++ b/test/smoke/js/SmokeTests.js @@ -10,25 +10,30 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -if (Object.prototype.should == null) { chai.should(); } -const { expect } = chai; -const request = require("request"); -const Settings = require("settings-sharelatex"); +const chai = require('chai') +if (Object.prototype.should == null) { + chai.should() +} +const { expect } = chai +const request = require('request') +const Settings = require('settings-sharelatex') -const buildUrl = path => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}`; +const buildUrl = path => + `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}` -const url = buildUrl(`project/smoketest-${process.pid}/compile`); +const url = buildUrl(`project/smoketest-${process.pid}/compile`) -describe("Running a compile", function() { - before(function(done) { - return request.post({ - url, - json: { - compile: { - resources: [{ - path: "main.tex", - content: `\ +describe('Running a compile', function() { + before(function(done) { + return request.post( + { + url, + json: { + compile: { + resources: [ + { + path: 'main.tex', + content: `\ % Membrane-like surface % Author: Yotam Avital \\documentclass{article} @@ -61,29 +66,35 @@ describe("Running a compile", function() { \\end{tikzpicture} \\end{document}\ ` - } - ] - } - } - }, (error, response, body) => { - this.error = error; - this.response = response; - this.body = body; - return done(); - }); - }); + } + ] + } + } + }, + (error, response, body) => { + this.error = error + this.response = response + this.body = body + return done() + } + ) + }) - it("should return the pdf", function() { - for (const file of Array.from(this.body.compile.outputFiles)) { - if (file.type === "pdf") { return; } - } - throw new Error("no pdf returned"); - }); - - return it("should return the log", function() { - for (const file of Array.from(this.body.compile.outputFiles)) { - if (file.type === "log") { return; } - } - throw new Error("no log returned"); - }); -}); + it('should return the pdf', function() { + for (const file of Array.from(this.body.compile.outputFiles)) { + if (file.type === 'pdf') { + return + } + } + throw new Error('no pdf returned') + }) + + return it('should return the log', function() { + for (const file of Array.from(this.body.compile.outputFiles)) { + if (file.type === 'log') { + return + } + } + throw new Error('no log returned') + }) +}) From 9fc13845a19f01dcbb977d68d4f1d21aa39f4cac Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 11 Mar 2020 10:06:55 +0000 Subject: [PATCH 499/709] remove ./bin/install_texlive_gce.sh which shouldn't be needed we shouldn't have needed this for a while, I think it is a cause of startup delay, however this should have stopped other missing texlive images in the past which is strange --- bin/install_texlive_gce.sh | 21 --------------------- entrypoint.sh | 1 - 2 files changed, 22 deletions(-) delete mode 100755 bin/install_texlive_gce.sh diff --git a/bin/install_texlive_gce.sh b/bin/install_texlive_gce.sh deleted file mode 100755 index ee6efba4..00000000 --- a/bin/install_texlive_gce.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh -METADATA=http://metadata.google.internal./computeMetadata/v1 -SVC_ACCT=$METADATA/instance/service-accounts/default -PROJECT_URL=$METADATA/project/project-id -ACCESS_TOKEN=$(curl -s -H 'Metadata-Flavor: Google' $SVC_ACCT/token | cut -d'"' -f 4) -if [ -z "$ACCESS_TOKEN" ]; then - echo "No acccess token to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." - exit 0 -fi -PROJECT=$(curl -s -H 'Metadata-Flavor: Google' $PROJECT_URL) -if [ -z "$PROJECT" ]; then - echo "No project name to download texlive-full images from google container, continuing without downloading. This is likely not a google cloud enviroment." - exit 0 -fi -docker login -u '_token' -p $ACCESS_TOKEN https://gcr.io -docker pull --all-tags gcr.io/$PROJECT/texlive-full -cp /app/bin/synctex /app/bin/synctex-mount/synctex - -echo "Finished downloading texlive-full images" - - diff --git a/entrypoint.sh b/entrypoint.sh index 71ced141..15a41db9 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -21,5 +21,4 @@ chown -R node:node /app chown -R node:node /app/bin -./bin/install_texlive_gce.sh exec runuser -u node -- "$@" From 7a0724d07b51b988a9138f880dbf144ff69787db Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 11 Mar 2020 11:55:44 +0100 Subject: [PATCH 500/709] updated build-scripts --- .eslintrc | 1 - .prettierrc | 1 - Dockerfile | 1 - Makefile | 1 - buildscript.txt | 10 +- docker-compose.ci.yml | 1 - docker-compose.yml | 1 - package-lock.json | 207 +++++++++++++++++++++++++++++------------- package.json | 10 +- 9 files changed, 154 insertions(+), 79 deletions(-) diff --git a/.eslintrc b/.eslintrc index 42a4b5ca..2e945d6f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,6 @@ // this file was auto-generated, do not edit it directly. // instead run bin/update_build_scripts from // https://github.com/sharelatex/sharelatex-dev-environment -// Version: 1.3.5 { "extends": [ "standard", diff --git a/.prettierrc b/.prettierrc index 5845b821..24f9ec52 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,6 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.3.5 { "semi": false, "singleQuote": true diff --git a/Dockerfile b/Dockerfile index 9faccd49..27158b5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.3.5 FROM node:10.19.0 as base diff --git a/Makefile b/Makefile index 88234f2a..0f1d274f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.3.5 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) diff --git a/buildscript.txt b/buildscript.txt index 8db0cd0e..52686425 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,10 +1,10 @@ clsi ---public-repo=True ---language=es ---env-add= ---node-version=10.19.0 --acceptance-creds=None --dependencies= --docker-repos=gcr.io/overleaf-ops +--env-add= --env-pass-through=TEXLIVE_IMAGE ---script-version=1.3.5 +--language=es +--node-version=10.19.0 +--public-repo=True +--script-version=2.0.0 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index aa7243e1..facbd5fc 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,6 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.3.5 version: "2.3" diff --git a/docker-compose.yml b/docker-compose.yml index 74b4471e..6fc9eab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ # This file was auto-generated, do not edit it directly. # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -# Version: 1.3.5 version: "2.3" diff --git a/package-lock.json b/package-lock.json index 05e00cd7..c9f9978a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -851,6 +851,12 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/console-log-level": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz", @@ -1039,12 +1045,20 @@ } }, "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-regex": { @@ -1973,9 +1987,9 @@ "dev": true }, "eslint": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", - "integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -1993,7 +2007,7 @@ "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", - "globals": "^11.7.0", + "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -2006,7 +2020,7 @@ "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", + "optionator": "^0.8.3", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^6.1.2", @@ -2018,9 +2032,9 @@ }, "dependencies": { "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -2050,6 +2064,15 @@ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", @@ -2252,12 +2275,24 @@ } }, "eslint-plugin-mocha": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.2.2.tgz", - "integrity": "sha512-oNhPzfkT6Q6CJ0HMVJ2KLxEWG97VWGTmuHOoRcDLE0U88ugUyFNV9wrT2XIt5cGtqc5W9k38m4xTN34L09KhBA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.3.0.tgz", + "integrity": "sha512-Cd2roo8caAyG21oKaaNTj7cqeYRWW1I2B5SfpKRp0Ip1gkfwoR1Ow0IGlPWnNjzywdF4n+kHL8/9vM6zCJUxdg==", "dev": true, "requires": { - "ramda": "^0.26.1" + "eslint-utils": "^2.0.0", + "ramda": "^0.27.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } } }, "eslint-plugin-node": { @@ -2382,20 +2417,26 @@ "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" }, "dependencies": { "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true } } @@ -3104,23 +3145,23 @@ "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" }, "inquirer": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", - "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^3.0.0", "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.15", "mute-stream": "0.0.8", - "run-async": "^2.2.0", + "run-async": "^2.4.0", "rxjs": "^6.5.3", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { @@ -3130,6 +3171,47 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3142,6 +3224,15 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "run-async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -3151,34 +3242,24 @@ "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } @@ -5189,9 +5270,9 @@ "dev": true }, "ramda": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", - "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", + "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==", "dev": true }, "range-parser": { @@ -5815,19 +5896,19 @@ "dev": true }, "sqlite3": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.6.tgz", - "integrity": "sha512-EqBXxHdKiwvNMRCgml86VTL5TK1i0IKiumnfxykX0gh6H6jaKijAXvE9O1N7+omfNSawR2fOmIyJZcfe8HYWpw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz", + "integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==", "requires": { - "nan": "~2.10.0", + "nan": "^2.12.1", "node-pre-gyp": "^0.11.0", "request": "^2.87.0" }, "dependencies": { "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" } } }, diff --git a/package.json b/package.json index 87403ca5..6ff6ea24 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", "lint": "node_modules/.bin/eslint .", - "format": "node_modules/.bin/prettier-eslint '**/*.js' --list-different", - "format:fix": "node_modules/.bin/prettier-eslint '**/*.js' --write" + "format": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --list-different", + "format:fix": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --write" }, "author": "James Allen <james@sharelatex.com>", "dependencies": { @@ -35,7 +35,7 @@ "sequelize": "^4.38.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "sqlite3": "^4.0.6", + "sqlite3": "^4.1.1", "underscore": "^1.8.2", "v8-profiler-node8": "^6.0.1", "wrench": "~1.5.4" @@ -44,7 +44,7 @@ "babel-eslint": "^10.0.3", "bunyan": "^0.22.1", "chai": "~1.8.1", - "eslint": "^6.6.0", + "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.0", "eslint-config-standard": "^14.1.0", "eslint-config-standard-jsx": "^8.1.0", @@ -53,7 +53,7 @@ "eslint-plugin-chai-friendly": "^0.5.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-mocha": "^6.2.2", + "eslint-plugin-mocha": "^6.3.0", "eslint-plugin-node": "^11.0.0", "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-promise": "^4.2.1", From 8673a99de3f50673bf0f98ba970bbd09568c3c68 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 11 Mar 2020 11:58:42 +0100 Subject: [PATCH 501/709] npm audit fix --- package-lock.json | 139 +++++++++++++++++++++------------------------- package.json | 4 +- 2 files changed, 66 insertions(+), 77 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9f9978a..7d3c0e26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -879,7 +879,7 @@ "@types/geojson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", - "integrity": "sha1-PgKXJyjGkkjCrwjWCkjLuGgP/98=" + "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" }, "@types/json-schema": { "version": "7.0.4", @@ -1015,9 +1015,9 @@ } }, "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" }, "acorn-jsx": { "version": "5.1.0", @@ -1282,9 +1282,9 @@ } }, "bluebird": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", - "integrity": "sha1-fQHG+WFsmlGrD4xUmnnf5uwz76c=" + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "body-parser": { "version": "1.18.3", @@ -1847,9 +1847,9 @@ } }, "dottie": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz", - "integrity": "sha1-aXrZ1yAE23V00h+JJGajwoWJNlk=" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", + "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" }, "dtrace-provider": { "version": "0.2.8", @@ -2805,9 +2805,9 @@ } }, "generic-pool": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.4.2.tgz", - "integrity": "sha1-kv9xllINZwg5pnMICSoSqt8valk=" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.5.0.tgz", + "integrity": "sha512-dEkxmX+egB2o4NR80c/q+xzLLzLX+k68/K8xv81XprD+Sk7ZtP14VugeCz+fUwv5FzpWq40pPtAkzPRqT8ka9w==" }, "get-caller-file": { "version": "2.0.5", @@ -3595,9 +3595,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.at": { "version": "4.6.0", @@ -3999,9 +3999,9 @@ "integrity": "sha1-dZ6kkayX1UusWtd2mW4qWMwbwiU=" }, "moment-timezone": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz", - "integrity": "sha1-fLsA2ywUxxsZMDy0ew+wpthlFGM=", + "version": "0.5.28", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.28.tgz", + "integrity": "sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==", "requires": { "moment": ">= 2.9.0" } @@ -4062,40 +4062,34 @@ } }, "mysql": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.6.2.tgz", - "integrity": "sha1-k3Gd0yT1fUHET7bX+GDPjmOFk7A=", + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", "requires": { - "bignumber.js": "2.0.7", - "readable-stream": "~1.1.13", - "require-all": "~1.0.0" + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" }, "dependencies": { "bignumber.js": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz", - "integrity": "sha1-husHB89qURCQnSPm6nQ0wU9QDxw=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" }, "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, @@ -5425,11 +5419,6 @@ } } }, - "require-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/require-all/-/require-all-1.0.0.tgz", - "integrity": "sha1-hINwjnzkxt+tmItQgPl4KbktIic=" - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5639,16 +5628,16 @@ } }, "sequelize": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.42.0.tgz", - "integrity": "sha1-Q5Rnunv+fVr8xW1is+CRhg+/GPM=", + "version": "4.44.4", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.44.4.tgz", + "integrity": "sha512-nkHmYkbwQK7uwpgW9VBalCBnQqQ8mslTdgcBthtJLORuPvAYRPlfkXZMVUU9TLLJt9CX+/y0MYg0DpcP6ywsEQ==", "requires": { "bluebird": "^3.5.0", "cls-bluebird": "^2.1.0", "debug": "^3.1.0", "depd": "^1.1.0", "dottie": "^2.0.0", - "generic-pool": "^3.4.0", + "generic-pool": "3.5.0", "inflection": "1.12.0", "lodash": "^4.17.1", "moment": "^2.20.0", @@ -5665,20 +5654,15 @@ "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -5912,6 +5896,11 @@ } } }, + "sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" + }, "sshpk": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", @@ -6197,17 +6186,17 @@ } }, "terraformer": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.9.tgz", - "integrity": "sha1-d4Uf70pJyQs0XcU88mgJ/fKdzaY=", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.10.tgz", + "integrity": "sha512-5c6cAfKTZHAeRdT8sIRRidhN1w+vsmf3RmQn+PKksFhTUnsBtjQdbJG2vaxM6T47IU2EeR1S8t8UjTYY9Q1yJA==", "requires": { - "@types/geojson": "^1.0.0" + "@types/geojson": "^7946.0.0 || ^1.0.0" } }, "terraformer-wkt-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz", - "integrity": "sha1-ydasPf8l9MC9NE6WH0JpSWGDTDQ=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.1.tgz", + "integrity": "sha512-+CJyNLWb3lJ9RsZMTM66BY0MT3yIo4l4l22Jd9CrZuwzk54fsu4Sc7zejuS9fCITTuTQy3p06d4MZMVI7v5wSg==", "requires": { "@types/geojson": "^1.0.0", "terraformer": "~1.0.5" @@ -6445,9 +6434,9 @@ } }, "validator": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.9.0.tgz", - "integrity": "sha1-0QwRZztQYft8z0wRFEEkEbK6wqg=" + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" }, "vary": { "version": "1.1.2", @@ -6567,9 +6556,9 @@ } }, "wkx": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.6.tgz", - "integrity": "sha1-Ioq1kuZFc4Lqb7efyCUFjQf85SM=", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz", + "integrity": "sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==", "requires": { "@types/node": "*" } diff --git a/package.json b/package.json index 6ff6ea24..350cefa5 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,9 @@ "lynx": "0.0.11", "metrics-sharelatex": "^2.3.0", "mkdirp": "0.3.5", - "mysql": "2.6.2", + "mysql": "^2.18.1", "request": "^2.21.0", - "sequelize": "^4.38.0", + "sequelize": "^4.44.4", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", "sqlite3": "^4.1.1", From 26122bef3a5d397d8762693e2eff5baa41a785d3 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Wed, 11 Mar 2020 11:06:46 +0000 Subject: [PATCH 502/709] copy synctex over to /app/bin/synctex-mount in entrypoint --- entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/entrypoint.sh b/entrypoint.sh index 15a41db9..07d902ac 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -21,4 +21,6 @@ chown -R node:node /app chown -R node:node /app/bin +cp /app/bin/synctex /app/bin/synctex-mount/synctex + exec runuser -u node -- "$@" From f3cf7ef9a0352012905f9645e5253c9d69b9baa3 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 11 Mar 2020 12:20:48 +0100 Subject: [PATCH 503/709] updated minor/patch dependencies --- package-lock.json | 597 +++++++++++++++++++++++++++------------------- package.json | 26 +- 2 files changed, 363 insertions(+), 260 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7d3c0e26..ac92dddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,23 +14,17 @@ } }, "@babel/generator": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", - "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", + "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", "dev": true, "requires": { - "@babel/types": "^7.8.3", + "@babel/types": "^7.8.7", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" }, "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -80,9 +74,9 @@ } }, "@babel/parser": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", - "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", "dev": true }, "@babel/runtime": { @@ -94,29 +88,47 @@ "regenerator-runtime": "^0.13.2" } }, + "@babel/runtime-corejs3": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.8.7.tgz", + "integrity": "sha512-sc7A+H4I8kTd7S61dgB9RomXu/C+F4IrRr4Ytze4dnfx7AXEpCrejSNpjx7vq6y/Bak9S6Kbk65a/WgMLtg43Q==", + "dev": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", + "integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==", + "dev": true + } + } + }, "@babel/template": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", - "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" } }, "@babel/traverse": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", - "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.4", + "@babel/generator": "^7.8.6", "@babel/helper-function-name": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.8.4", - "@babel/types": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" @@ -131,12 +143,6 @@ "ms": "^2.1.1" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -146,22 +152,14 @@ } }, "@babel/types": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", - "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - } } }, "@google-cloud/common": { @@ -230,9 +228,9 @@ "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==" }, "coffeescript": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.4.1.tgz", - "integrity": "sha512-34GV1aHrsMpTaO3KfMJL40ZNuvKDR/g98THHnE9bQj8HjMaZvSrLik99WWqyMhRtbe8V5hpx5iLgdcSvM/S2wg==" + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", + "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" }, "debug": { "version": "3.2.6", @@ -898,9 +896,9 @@ "integrity": "sha1-IOhWUbYv2GZW5Xycm8dxqxVwvFk=" }, "@types/request": { - "version": "2.48.3", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.3.tgz", - "integrity": "sha512-3Wo2jNYwqgXcIz/rrq18AdOZUQB8cQ34CXZo+LUwPJNpvRAL86+Kc2wwI8mqpz9Cr1V+enIox5v+WZhy/p3h8w==", + "version": "2.48.4", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.4.tgz", + "integrity": "sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw==", "requires": { "@types/caseless": "*", "@types/node": "*", @@ -926,9 +924,9 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==" }, "@types/tough-cookie": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", - "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz", + "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==" }, "@typescript-eslint/experimental-utils": { "version": "1.13.0", @@ -1006,12 +1004,12 @@ } }, "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" + "mime-types": "~2.1.24", + "negotiator": "0.6.2" } }, "acorn": { @@ -1034,11 +1032,11 @@ } }, "ajv": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", - "integrity": "sha1-ys7M9HS/P8POOxR0Q3EaJAY8ww0=", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -1150,7 +1148,7 @@ "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "requires": { "safer-buffer": "~2.1.0" } @@ -1179,9 +1177,9 @@ "dev": true }, "async": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz", - "integrity": "sha1-32MGD789Myhqdqr21Vophtn/hhk=" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" }, "async-listener": { "version": "0.6.10", @@ -1203,9 +1201,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, "axios": { "version": "0.18.1", @@ -1223,15 +1221,15 @@ "dev": true }, "babel-eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz", - "integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", "eslint-visitor-keys": "^1.0.0", "resolve": "^1.12.0" } @@ -1287,20 +1285,30 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "requires": { - "bytes": "3.0.0", + "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } } }, "boolify": { @@ -1359,13 +1367,15 @@ "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==" }, "bunyan": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.3.tgz", - "integrity": "sha1-ehncG0yMZF90AkGnQPIkUUfGfsI=", + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", + "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", "dev": true, "requires": { - "dtrace-provider": "0.2.8", - "mv": "~2" + "dtrace-provider": "~0.8", + "moment": "^2.10.6", + "mv": "~2", + "safe-json-stringify": "~1" } }, "buster-core": { @@ -1384,9 +1394,9 @@ } }, "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "callsites": { "version": "3.1.0", @@ -1566,9 +1576,9 @@ "dev": true }, "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha1-LR0kMXr7ir6V1tLAsHtXgTU52Cg=", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } @@ -1617,14 +1627,17 @@ "dev": true }, "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "continuation-local-storage": { "version": "3.2.1", @@ -1651,6 +1664,12 @@ "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", "dev": true }, + "core-js-pure": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz", + "integrity": "sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1852,11 +1871,23 @@ "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" }, "dtrace-provider": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", - "integrity": "sha1-4kPxkhmqlfvw2PL/sH9b1k6U/iA=", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", "dev": true, - "optional": true + "optional": true, + "requires": { + "nan": "^2.14.0" + }, + "dependencies": { + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + } + } }, "duplexify": { "version": "3.7.1", @@ -2348,9 +2379,9 @@ "dev": true }, "eslint-plugin-react": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.3.tgz", - "integrity": "sha512-Bt56LNHAQCoou88s8ViKRjMB2+36XRejCQ1VoLj716KI1MoE99HpTVvIThJ0rvFmG4E4Gsq+UgToEjn+j044Bg==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", + "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==", "dev": true, "requires": { "array-includes": "^3.1.1", @@ -2361,8 +2392,10 @@ "object.fromentries": "^2.0.2", "object.values": "^1.1.1", "prop-types": "^15.7.2", - "resolve": "^1.14.2", - "string.prototype.matchall": "^4.0.2" + "resolve": "^1.15.1", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.2", + "xregexp": "^4.3.0" }, "dependencies": { "doctrine": { @@ -2382,6 +2415,12 @@ "requires": { "path-parse": "^1.0.6" } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -2497,46 +2536,46 @@ } }, "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha1-/d72GSYQniTFFeqX/S8b2/Yt8S4=", + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "requires": { - "accepts": "~1.3.5", + "accepts": "~1.3.7", "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", "content-type": "~1.0.4", - "cookie": "0.3.1", + "cookie": "0.4.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.1.1", + "finalhandler": "~1.1.2", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" } } }, @@ -2573,9 +2612,9 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-diff": { "version": "1.2.0", @@ -2623,24 +2662,17 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "finalhandler": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha1-7r9O2EAHnIP0JJA4ydcDAIMBsQU=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" - } } }, "find-up": { @@ -2711,7 +2743,7 @@ "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -2946,7 +2978,7 @@ "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -3013,14 +3045,15 @@ "dev": true }, "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "requires": { "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" } }, "http-proxy-agent": { @@ -3276,9 +3309,9 @@ } }, "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, "is": { "version": "3.3.0", @@ -3680,6 +3713,38 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", "optional": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } } } }, @@ -3829,9 +3894,9 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "metrics-sharelatex": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.3.0.tgz", - "integrity": "sha512-qQv4UhI0Pn89WtIEUkysy4fgaCxmIw2S7+2pUB5b15Q4dzleHNplop5peTEOf8FIcURFshjPPJiLOGCJAYph7Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.5.1.tgz", + "integrity": "sha512-C2gmkl/tUnq3IlSX/x3dixGhdvfD6H9FR9mBf9lnkeyy2arafxhCU6u+1IQj6byjBM7vGpYHyjwWnmoi3Vb+ZQ==", "requires": { "@google-cloud/debug-agent": "^3.0.0", "@google-cloud/profiler": "^0.2.3", @@ -3860,21 +3925,21 @@ } }, "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha1-C2oM5v2+lXbiXx8tL96IMNwK0Ng=" + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" }, "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha1-KJlaoey3cHQv5q5+WPkYHHRLP5Y=", + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", "requires": { - "mime-db": "~1.37.0" + "mime-db": "1.43.0" } }, "mimic-fn": { @@ -4126,9 +4191,9 @@ } }, "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "nice-try": { "version": "1.0.5", @@ -4227,7 +4292,7 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", @@ -4401,9 +4466,9 @@ } }, "parse-duration": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz", - "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.2.tgz", + "integrity": "sha512-0qfMZyjOUFBeEIvJ5EayfXJqaEXxQ+Oj2b7tWJM3hvEXvXsYCk05EDVI23oYnEw2NaFYUWdABEVPBvBMh8L/pA==" }, "parse-json": { "version": "2.2.0", @@ -4420,9 +4485,9 @@ "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" }, "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-exists": { "version": "3.0.0", @@ -5183,18 +5248,18 @@ } }, "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha1-7PxzO/Iv+Mb0B/onUye5q2fki5M=", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" + "ipaddr.js": "1.9.1" } }, "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha1-6aqG0BAbWxBcvpOsa3hM1UcnYYQ=" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" }, "pump": { "version": "1.0.3", @@ -5253,9 +5318,9 @@ "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=" + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "quick-lru": { "version": "4.0.1", @@ -5270,9 +5335,9 @@ "dev": true }, "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raven": { "version": "1.1.3", @@ -5294,14 +5359,24 @@ } }, "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha1-GzJOzmtXBuFThVvBFIxlu39uoMM=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } } }, "rc": { @@ -5323,9 +5398,9 @@ } }, "react-is": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", - "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz", + "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==", "dev": true }, "read-pkg": { @@ -5386,9 +5461,9 @@ "dev": true }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -5397,7 +5472,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -5407,15 +5482,24 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } } } }, @@ -5601,9 +5685,9 @@ "integrity": "sha1-fnQlb7qknHWqfHogXMInmcrIAAQ=" }, "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "requires": { "debug": "2.6.9", "depd": "~1.1.2", @@ -5612,18 +5696,18 @@ "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" + "range-parser": "~1.2.1", + "statuses": "~1.5.0" }, "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, @@ -5667,14 +5751,14 @@ } }, "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" + "parseurl": "~1.3.3", + "send": "0.17.1" } }, "set-blocking": { @@ -5683,9 +5767,9 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, "settings-sharelatex": { "version": "git+https://github.com/sharelatex/settings-sharelatex.git#93f63d029b52fef8825c3a401b2b6a7ba29b4750", @@ -5902,9 +5986,9 @@ "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, "sshpk": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", - "integrity": "sha1-yUbWvZsaOdDoY1dj9SQtbtbctik=", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -6222,9 +6306,9 @@ } }, "timekeeper": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-0.0.4.tgz", - "integrity": "sha1-kNt58X2Ni1NiFUOJSSuXJ2LP0nY=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.2.0.tgz", + "integrity": "sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A==", "dev": true }, "tmp": { @@ -6268,6 +6352,11 @@ "to-no-case": "^1.0.0" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "toposort-class": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", @@ -6276,7 +6365,7 @@ "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -6329,12 +6418,12 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" }, "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "requires": { "media-typer": "0.3.0", - "mime-types": "~2.1.18" + "mime-types": "~2.1.24" } }, "typedarray": { @@ -6349,9 +6438,9 @@ "dev": true }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE=" + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz", + "integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==" }, "unpipe": { "version": "1.0.0", @@ -6388,12 +6477,12 @@ "dev": true }, "v8-profiler-node8": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/v8-profiler-node8/-/v8-profiler-node8-6.0.1.tgz", - "integrity": "sha512-DD7L0c/2KeFFQxs20VaFPHq/CSintttW4YB+QJdJ5ZohxOegbsMCvnZKHRHVA9UZfVYqq6ZsLaywe74+3JpZjw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/v8-profiler-node8/-/v8-profiler-node8-6.1.1.tgz", + "integrity": "sha512-mKS7TXRRYi70hvbv5c1tk9AbuqNrtbLc+jFLlsZ2TpaC1l5lWryBlDLZKJ1JP6hjSbMEjW1ucjWLSaKsaPnGXg==", "requires": { - "nan": "^2.5.1", - "node-pre-gyp": "^0.11.0" + "nan": "^2.14.0", + "node-pre-gyp": "^0.13.0" }, "dependencies": { "mkdirp": { @@ -6404,10 +6493,15 @@ "minimist": "0.0.8" } }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz", + "integrity": "sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==", "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -6650,6 +6744,15 @@ } } }, + "xregexp": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", + "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", + "dev": true, + "requires": { + "@babel/runtime-corejs3": "^7.8.3" + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index 350cefa5..54084109 100644 --- a/package.json +++ b/package.json @@ -19,30 +19,30 @@ }, "author": "James Allen <james@sharelatex.com>", "dependencies": { - "async": "0.2.9", - "body-parser": "^1.2.0", + "async": "3.2.0", + "body-parser": "^1.19.0", "dockerode": "^2.5.3", - "express": "^4.2.0", + "express": "^4.17.1", "fs-extra": "^0.16.3", "heapdump": "^0.3.5", - "lockfile": "^1.0.3", + "lockfile": "^1.0.4", "logger-sharelatex": "^1.9.0", "lynx": "0.0.11", - "metrics-sharelatex": "^2.3.0", + "metrics-sharelatex": "^2.5.1", "mkdirp": "0.3.5", "mysql": "^2.18.1", - "request": "^2.21.0", + "request": "^2.88.2", "sequelize": "^4.44.4", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", "sqlite3": "^4.1.1", - "underscore": "^1.8.2", - "v8-profiler-node8": "^6.0.1", - "wrench": "~1.5.4" + "underscore": "^1.9.2", + "v8-profiler-node8": "^6.1.1", + "wrench": "~1.5.9" }, "devDependencies": { - "babel-eslint": "^10.0.3", - "bunyan": "^0.22.1", + "babel-eslint": "^10.1.0", + "bunyan": "^1.8.12", "chai": "~1.8.1", "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.0", @@ -57,13 +57,13 @@ "eslint-plugin-node": "^11.0.0", "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-react": "^7.18.3", + "eslint-plugin-react": "^7.19.0", "eslint-plugin-standard": "^4.0.1", "mocha": "^4.0.1", "prettier": "^1.19.1", "prettier-eslint-cli": "^5.0.0", "sandboxed-module": "~0.3.0", "sinon": "~1.7.3", - "timekeeper": "0.0.4" + "timekeeper": "2.2.0" } } From c49381cfba0344ad458c5601f06d49506c1bdb5a Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 11 Mar 2020 19:25:50 +0100 Subject: [PATCH 504/709] updated dockerode, heapdump, lyns and fs-extra --- package-lock.json | 284 +++++++++++++++++++++++----------------------- package.json | 8 +- 2 files changed, 145 insertions(+), 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac92dddb..ae748df8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -981,15 +981,6 @@ } } }, - "JSONStream": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", - "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1271,12 +1262,23 @@ "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" }, "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.1.tgz", + "integrity": "sha512-FL/TdvchukRCuWVxT0YMO/7+L5TNeNrVFvRU2IY63aUyv9mpt8splf2NEr6qXtPo5fya5a66YohQKvGNmLrWNA==", "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "bluebird": { @@ -1332,34 +1334,15 @@ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA=" - }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=" + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "builtin-modules": { "version": "3.1.0", @@ -1600,14 +1583,26 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "console-control-strings": { @@ -1792,60 +1787,49 @@ "dev": true }, "docker-modem": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.7.tgz", - "integrity": "sha1-aXAqlcBg7rZ3X3nM3Mc05ZRpcqQ=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.1.tgz", + "integrity": "sha512-zSFwYN4AP38LJhTIOpZMjiDbAqSJbv8+u9i/Xq5XABIeTzgp83VF63epu6sVHWxe+6tfhMXqgV+sYjZWh/UzSQ==", "requires": { - "JSONStream": "1.3.2", - "debug": "^3.2.5", - "readable-stream": "~1.0.26-4", - "split-ca": "^1.0.0" + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^0.8.7" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, "dockerode": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.7.tgz", - "integrity": "sha1-E9yewPfzU6wOUSJJ538y0aqhGZ4=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.1.0.tgz", + "integrity": "sha512-E0KknBBTlIVEvtt2XJRZ3he59u2UN8Yr1A08Sey/BKIox+WlwnJp5fL5SKyhPgNmSXgamPEuKYCJxMi31uj0Nw==", "requires": { - "concat-stream": "~1.6.2", - "docker-modem": "1.0.x", - "tar-fs": "~1.16.3" + "concat-stream": "~2.0.0", + "docker-modem": "^2.1.0", + "tar-fs": "~2.0.0" } }, "doctrine": { @@ -2763,16 +2747,16 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.16.5.tgz", - "integrity": "sha1-GtZh+myGyWCM0bSe/G/Og0k5p1A=", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "requires": { - "graceful-fs": "^3.0.5", - "jsonfile": "^2.0.0", - "rimraf": "^2.2.8" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, "fs-minipass": { @@ -2940,12 +2924,9 @@ } }, "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "requires": { - "natives": "^1.1.0" - } + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "growl": { "version": "1.7.0", @@ -3026,11 +3007,18 @@ "dev": true }, "heapdump": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.12.tgz", - "integrity": "sha1-ViO+eBaoqSqy1CsbQi+egppY2ok=", + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz", + "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==", "requires": { - "nan": "^2.11.1" + "nan": "^2.13.2" + }, + "dependencies": { + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + } } }, "hex2dec": { @@ -3513,26 +3501,13 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { "graceful-fs": "^4.1.6" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha1-/7cD4QZuig7qpMi4C6klPu77+wA=", - "optional": true - } } }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -3819,9 +3794,9 @@ "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" }, "lynx": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.0.11.tgz", - "integrity": "sha1-LPoU5EP9LZKlm3efQVZ84cxpZaM=", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.2.0.tgz", + "integrity": "sha1-eeZnRTDaQYPoeVO9aGFx4HDaULk=", "requires": { "mersenne": "~0.0.3", "statsd-parser": "~0.0.4" @@ -4163,11 +4138,6 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", "integrity": "sha1-nUQ/214TogdwzF5gLu5ZdgpoWIU=" }, - "natives": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", - "integrity": "sha1-pgO0pJirdxc2ErnqGs3sTZgPALs=" - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5262,9 +5232,9 @@ "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" }, "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha1-Xf6DEcM7v2/BgmH580cCxHwIqVQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5985,6 +5955,24 @@ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, + "ssh2": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.8.tgz", + "integrity": "sha512-egJVQkf3sbjECTY6rCeg8rgV/fab6S/7E5kpYqHT3Fe/YpfJbLYeA1qTcB2d+LRUUAjqKi7rlbfWkaP66YdpAQ==", + "requires": { + "ssh2-streams": "~0.4.9" + } + }, + "ssh2-streams": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.9.tgz", + "integrity": "sha512-glMQKeYKuA+rLaH16fJC3nZMV1BWklbxuYCR4C5/LlBSf2yaoNRpPU7Ul702xsi5nvYjIx9XDkKBJwrBjkDynw==", + "requires": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -6029,6 +6017,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -6215,14 +6208,14 @@ } }, "tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha1-lmpiiEHaLEAQQGqCFny9Xgxy1Qk=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz", + "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==", "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" + "pump": "^3.0.0", + "tar-stream": "^2.0.0" }, "dependencies": { "mkdirp": { @@ -6236,17 +6229,27 @@ } }, "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "tdigest": { @@ -6320,11 +6323,6 @@ "os-tmpdir": "~1.0.2" } }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=" - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6442,6 +6440,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz", "integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==" }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6753,11 +6756,6 @@ "@babel/runtime-corejs3": "^7.8.3" } }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", diff --git a/package.json b/package.json index 54084109..bf4a532b 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,13 @@ "dependencies": { "async": "3.2.0", "body-parser": "^1.19.0", - "dockerode": "^2.5.3", + "dockerode": "^3.1.0", "express": "^4.17.1", - "fs-extra": "^0.16.3", - "heapdump": "^0.3.5", + "fs-extra": "^8.1.0", + "heapdump": "^0.3.15", "lockfile": "^1.0.4", "logger-sharelatex": "^1.9.0", - "lynx": "0.0.11", + "lynx": "0.2.0", "metrics-sharelatex": "^2.5.1", "mkdirp": "0.3.5", "mysql": "^2.18.1", From a850fec664f09675e4d42ef19f578fb3d9604924 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Wed, 11 Mar 2020 19:39:08 +0100 Subject: [PATCH 505/709] updated mkdirp --- package-lock.json | 13 ++++++++++--- package.json | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae748df8..42d16ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3954,9 +3954,9 @@ } }, "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" }, "mocha": { "version": "4.1.0", @@ -5864,6 +5864,13 @@ "growl": "1.7.x", "jade": "0.26.3", "mkdirp": "0.3.5" + }, + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" + } } } } diff --git a/package.json b/package.json index bf4a532b..659726b7 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "logger-sharelatex": "^1.9.0", "lynx": "0.2.0", "metrics-sharelatex": "^2.5.1", - "mkdirp": "0.3.5", + "mkdirp": "1.0.3", "mysql": "^2.18.1", "request": "^2.88.2", "sequelize": "^4.44.4", From ffb33ddb404d2e3bd674cee4aaecfb0317c5e399 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Thu, 12 Mar 2020 10:22:08 +0100 Subject: [PATCH 506/709] removed mkdirp dependency and replaced with fs.mkdir --- app/js/ResourceWriter.js | 3 +-- package-lock.json | 5 ----- package.json | 1 - test/unit/js/ResourceWriterTests.js | 7 ++++--- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index ba9706bc..5a234e0d 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -19,7 +19,6 @@ const UrlCache = require('./UrlCache') const Path = require('path') const fs = require('fs') const async = require('async') -const mkdirp = require('mkdirp') const OutputFileFinder = require('./OutputFileFinder') const ResourceStateManager = require('./ResourceStateManager') const Metrics = require('./Metrics') @@ -302,7 +301,7 @@ module.exports = ResourceWriter = { if (error != null) { return callback(error) } - return mkdirp(Path.dirname(path), function(error) { + return fs.mkdir(Path.dirname(path), { recursive: true }, function(error) { if (error != null) { return callback(error) } diff --git a/package-lock.json b/package-lock.json index 42d16ee8..c7b9e4d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3953,11 +3953,6 @@ "minipass": "^2.2.1" } }, - "mkdirp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", - "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" - }, "mocha": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", diff --git a/package.json b/package.json index 659726b7..7317cae7 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "logger-sharelatex": "^1.9.0", "lynx": "0.2.0", "metrics-sharelatex": "^2.5.1", - "mkdirp": "1.0.3", "mysql": "^2.18.1", "request": "^2.88.2", "sequelize": "^4.44.4", diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 189908dc..b542f251 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -31,7 +31,6 @@ describe('ResourceWriter', function() { './ResourceStateManager': (this.ResourceStateManager = {}), wrench: (this.wrench = {}), './UrlCache': (this.UrlCache = {}), - mkdirp: (this.mkdirp = sinon.stub().callsArg(1)), './OutputFileFinder': (this.OutputFileFinder = {}), 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, './Metrics': (this.Metrics = { @@ -346,6 +345,7 @@ describe('ResourceWriter', function() { describe('_writeResourceToDisk', function() { describe('with a url based resource', function() { beforeEach(function() { + this.fs.mkdir = sinon.stub().callsArg(2) this.resource = { path: 'main.tex', url: 'http://www.example.com/main.tex', @@ -363,7 +363,7 @@ describe('ResourceWriter', function() { }) it('should ensure the directory exists', function() { - return this.mkdirp + this.fs.mkdir .calledWith( path.dirname(path.join(this.basePath, this.resource.path)) ) @@ -397,6 +397,7 @@ describe('ResourceWriter', function() { content: 'Hello world' } this.fs.writeFile = sinon.stub().callsArg(2) + this.fs.mkdir = sinon.stub().callsArg(2) return this.ResourceWriter._writeResourceToDisk( this.project_id, this.resource, @@ -406,7 +407,7 @@ describe('ResourceWriter', function() { }) it('should ensure the directory exists', function() { - return this.mkdirp + return this.fs.mkdir .calledWith( path.dirname(path.join(this.basePath, this.resource.path)) ) From 3ff9c18dcb45cf677571a352ed1af76eb7fee21e Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Thu, 12 Mar 2020 10:35:11 +0100 Subject: [PATCH 507/709] updated mocha and sinon, fixed tests --- package-lock.json | 448 ++++++++++++++++++---- package.json | 4 +- test/unit/js/CompileManagerTests.js | 6 +- test/unit/js/DockerRunnerTests.js | 18 +- test/unit/js/LockManagerTests.js | 7 +- test/unit/js/ResourceStateManagerTests.js | 32 +- test/unit/js/ResourceWriterTests.js | 36 +- test/unit/js/UrlFetcherTests.js | 10 +- 8 files changed, 442 insertions(+), 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7b9e4d4..a42045ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -844,6 +844,67 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.15.0.tgz", "integrity": "sha512-lu8BpxjAtRCAo5ifytTpCPCj99LF7o/2Myn+NXyNCBqvPYn7Pjd76AMmUB5l7XF1U6t0hcWrlEM5ESufW7wAeA==" }, + "@sinonjs/commons": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", + "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + }, + "dependencies": { + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + } + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.0.tgz", + "integrity": "sha512-atR1J/jRXvQAb47gfzSK8zavXy7BcpnYq21ALon0U99etu99vsir0trzIO3wpeLtW+LLVY6X7EkfVTbjGSH8Ww==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz", + "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -1033,6 +1094,12 @@ "uri-js": "^4.2.2" } }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, "ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -1064,6 +1131,16 @@ "color-convert": "^1.9.0" } }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -1248,6 +1325,12 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -1328,10 +1411,19 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "buffer-equal-constant-time": { @@ -1361,21 +1453,6 @@ "safe-json-stringify": "~1" } }, - "buster-core": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/buster-core/-/buster-core-0.6.4.tgz", - "integrity": "sha1-J79rrWdCROpyDzEdkAoMoct4YFA=", - "dev": true - }, - "buster-format": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/buster-format/-/buster-format-0.5.6.tgz", - "integrity": "sha1-K4bDIuz14bCubm55Bev884fSq5U=", - "dev": true, - "requires": { - "buster-core": "=0.6.4" - } - }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1453,6 +1530,22 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, "chownr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", @@ -2645,6 +2738,15 @@ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -2673,6 +2775,15 @@ "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz", "integrity": "sha1-WKRmaX34piBc39vzlVNri9d3pfY=" }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -2772,6 +2883,13 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -2984,9 +3102,9 @@ } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, "has-symbols": { @@ -3001,9 +3119,9 @@ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, "heapdump": { @@ -3312,6 +3430,15 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-bluebird": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", @@ -3357,6 +3484,12 @@ "is-extglob": "^2.1.1" } }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -3529,6 +3662,12 @@ "object.assign": "^4.1.0" } }, + "just-extend": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", + "dev": true + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -3617,6 +3756,12 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.has": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", @@ -3650,6 +3795,15 @@ "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==" }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + } + }, "logger-sharelatex": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.9.0.tgz", @@ -3954,64 +4108,77 @@ } }, "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha1-fYbPvPNcuCnidUwy4XNV7AUzh5Q=", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.0.tgz", + "integrity": "sha512-MymHK8UkU0K15Q/zX7uflZgVoRWiTjy0fXE/QjKts6mowUvGxOdPhZ2qj3b0iZdUrNZlW9LAIMFHB4IW+2b3EQ==", "dev": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "chokidar": "3.3.0", + "debug": "3.2.6", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "4.4.0" + "ms": "2.1.1", + "node-environment-flags": "1.0.6", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.0" }, "dependencies": { - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=", - "dev": true - }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "locate-path": "^3.0.0" } }, "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha1-GSa6kM8+3+KttJJ/WIC8IsZseQ8=", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", @@ -4020,6 +4187,21 @@ "requires": { "minimist": "0.0.8" } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } } } }, @@ -4166,6 +4348,54 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz", + "integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "node-environment-flags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -4224,6 +4454,12 @@ "validate-npm-package-license": "^3.0.1" } }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "npm-bundled": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", @@ -4312,6 +4548,16 @@ "has": "^1.0.3" } }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "object.values": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", @@ -4509,6 +4755,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picomatch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", + "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -5403,6 +5655,15 @@ "util-deprecate": "~1.0.1" } }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, "regenerator-runtime": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", @@ -5784,12 +6045,41 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "sinon": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.7.3.tgz", - "integrity": "sha1-emnWnNApRYbHQyVO7/G1g6UJl/I=", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.1.tgz", + "integrity": "sha512-iTTyiQo5T94jrOx7X7QLBZyucUJ2WvL9J13+96HMfm2CGoJYbIPqRfl6wgNcqmzk0DI28jeGx5bUTXizkrqBmg==", "dev": true, "requires": { - "buster-format": "~0.5" + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.0.3", + "diff": "^4.0.2", + "nise": "^4.0.1", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "slice-ansi": { @@ -6101,12 +6391,12 @@ "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" }, "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha1-iD992rwWUUKyphQn8zUt7RldGj4=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "requires": { - "has-flag": "^2.0.0" + "has-flag": "^3.0.0" } }, "table": { @@ -6336,6 +6626,15 @@ "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=" }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "to-snake-case": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz", @@ -6865,6 +7164,17 @@ "decamelize": "^1.2.0" } }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 7317cae7..bdbbd5ff 100644 --- a/package.json +++ b/package.json @@ -58,11 +58,11 @@ "eslint-plugin-promise": "^4.2.1", "eslint-plugin-react": "^7.19.0", "eslint-plugin-standard": "^4.0.1", - "mocha": "^4.0.1", + "mocha": "^7.1.0", "prettier": "^1.19.1", "prettier-eslint-cli": "^5.0.0", "sandboxed-module": "~0.3.0", - "sinon": "~1.7.3", + "sinon": "~9.0.1", "timekeeper": "2.2.0" } } diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index ae50bcc6..180f6f31 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -347,10 +347,10 @@ describe('CompileManager', function() { .should.equal(true) }) - return it('should call the callback with an error from the stderr', function() { - this.callback.calledWith(new Error()).should.equal(true) + it('should call the callback with an error from the stderr', function() { + this.callback.calledWithExactly(sinon.match(Error)).should.equal(true) - return this.callback.args[0][0].message.should.equal( + this.callback.args[0][0].message.should.equal( `rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} failed: ${this.error}` ) }) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index d17d906d..eea66b12 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -464,8 +464,8 @@ describe('DockerRunner', function() { return this.createContainer.called.should.equal(false) }) - return it('should call the callback with an error', function() { - return this.callback.calledWith(new Error()).should.equal(true) + it('should call the callback with an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) }) }) @@ -488,8 +488,8 @@ describe('DockerRunner', function() { return this.createContainer.called.should.equal(false) }) - return it('should call the callback with an error', function() { - return this.callback.calledWith(new Error()).should.equal(true) + it('should call the callback with an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) }) }) @@ -582,10 +582,12 @@ describe('DockerRunner', function() { return this.container.kill.called.should.equal(true) }) - return it('should call the callback with an error', function() { - const error = new Error('container timed out') - error.timedout = true - return this.callback.calledWith(error).should.equal(true) + it('should call the callback with an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) + + const errorObj = this.callback.args[0][0] + expect(errorObj.message).to.include('container timed out') + expect(errorObj.timedout).equal(true) }) }) }) diff --git a/test/unit/js/LockManagerTests.js b/test/unit/js/LockManagerTests.js index ea6c3419..cb8ab9b7 100644 --- a/test/unit/js/LockManagerTests.js +++ b/test/unit/js/LockManagerTests.js @@ -84,9 +84,10 @@ describe('DockerLockManager', function() { return this.runner.called.should.equal(false) }) - return it('should return an error', function() { - const error = new Errors.AlreadyCompilingError() - return this.callback.calledWithExactly(error).should.equal(true) + it('should return an error', function() { + this.callback + .calledWithExactly(sinon.match(Errors.AlreadyCompilingError)) + .should.equal(true) }) }) }) diff --git a/test/unit/js/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js index c0e89ef7..4c16ed1f 100644 --- a/test/unit/js/ResourceStateManagerTests.js +++ b/test/unit/js/ResourceStateManagerTests.js @@ -11,6 +11,7 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') +const { expect } = require('chai') const should = require('chai').should() const modulePath = require('path').join( __dirname, @@ -132,11 +133,13 @@ describe('ResourceStateManager', function() { ) }) - return it('should call the callback with an error', function() { - const error = new Errors.FilesOutOfSyncError( - 'invalid state for incremental update' - ) - return this.callback.calledWith(error).should.equal(true) + it('should call the callback with an error', function() { + this.callback + .calledWith(sinon.match(Errors.FilesOutOfSyncError)) + .should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include('invalid state for incremental update') }) }) }) @@ -174,11 +177,15 @@ describe('ResourceStateManager', function() { ) }) - return it('should call the callback with an error', function() { - const error = new Errors.FilesOutOfSyncError( + it('should call the callback with an error', function() { + this.callback + .calledWith(sinon.match(Errors.FilesOutOfSyncError)) + .should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include( 'resource files missing in incremental update' ) - return this.callback.calledWith(error).should.equal(true) }) }) @@ -198,10 +205,11 @@ describe('ResourceStateManager', function() { ) }) - return it('should call the callback with an error', function() { - return this.callback - .calledWith(new Error('relative path in resource file list')) - .should.equal(true) + it('should call the callback with an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include('relative path in resource file list') }) }) }) diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index b542f251..2844ce71 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -12,6 +12,7 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') +const { expect } = require('chai') const should = require('chai').should() const modulePath = require('path').join( __dirname, @@ -447,10 +448,11 @@ describe('ResourceWriter', function() { return this.fs.writeFile.called.should.equal(false) }) - return it('should return an error', function() { - return this.callback - .calledWith(new Error('resource path is outside root directory')) - .should.equal(true) + it('should return an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include('resource path is outside root directory') }) }) }) @@ -468,21 +470,18 @@ describe('ResourceWriter', function() { describe('with an invalid path', function() { beforeEach(function() { - return this.ResourceWriter.checkPath( - 'foo', - 'baz/../../bar', - this.callback - ) + this.ResourceWriter.checkPath('foo', 'baz/../../bar', this.callback) }) - return it('should return an error', function() { - return this.callback - .calledWith(new Error('resource path is outside root directory')) - .should.equal(true) + it('should return an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include('resource path is outside root directory') }) }) - return describe('with another invalid path matching on a prefix', function() { + describe('with another invalid path matching on a prefix', function() { beforeEach(function() { return this.ResourceWriter.checkPath( 'foo', @@ -491,10 +490,11 @@ describe('ResourceWriter', function() { ) }) - return it('should return an error', function() { - return this.callback - .calledWith(new Error('resource path is outside root directory')) - .should.equal(true) + it('should return an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include('resource path is outside root directory') }) }) }) diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index e5ce52b9..c435f450 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -10,6 +10,7 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') +const { expect } = require('chai') require('chai').should() const modulePath = require('path').join(__dirname, '../../../app/js/UrlFetcher') const { EventEmitter } = require('events') @@ -140,10 +141,11 @@ describe('UrlFetcher', function() { return this.urlStream.emit('end') }) - return it('should call the callback with an error', function() { - return this.callback - .calledWith(new Error('URL returned non-success status code: 404')) - .should.equal(true) + it('should call the callback with an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include('URL returned non-success status code: 404') }) }) From 6f837f1a74b6b303547317e39747603e5bf25627 Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Mon, 16 Mar 2020 16:31:02 +0100 Subject: [PATCH 508/709] updated sequelize --- app/js/ProjectPersistenceManager.js | 2 +- app/js/UrlCache.js | 4 +- package-lock.json | 108 +++++++++++++--------------- package.json | 2 +- 4 files changed, 52 insertions(+), 64 deletions(-) diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index 8015baa9..46eee747 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -32,7 +32,7 @@ module.exports = ProjectPersistenceManager = { db.Project.findOrCreate({ where: { project_id } }) .spread((project, created) => project - .updateAttributes({ lastAccessed: new Date() }) + .update({ lastAccessed: new Date() }) .then(() => cb()) .error(cb) ) diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index babdf9cf..9b982e5e 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -227,7 +227,7 @@ module.exports = UrlCache = { callback = function(error, urlDetails) {} } const job = cb => - db.UrlCache.find({ where: { url, project_id } }) + db.UrlCache.findOne({ where: { url, project_id } }) .then(urlDetails => cb(null, urlDetails)) .error(cb) return dbQueue.queue.push(job, callback) @@ -241,7 +241,7 @@ module.exports = UrlCache = { db.UrlCache.findOrCreate({ where: { url, project_id } }) .spread((urlDetails, created) => urlDetails - .updateAttributes({ lastModified }) + .update({ lastModified }) .then(() => cb()) .error(cb) ) diff --git a/package-lock.json b/package-lock.json index a42045ed..dcb2e946 100644 --- a/package-lock.json +++ b/package-lock.json @@ -935,11 +935,6 @@ "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", "dev": true }, - "@types/geojson": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", - "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" - }, "@types/json-schema": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", @@ -1131,6 +1126,11 @@ "color-convert": "^1.9.0" } }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", @@ -2938,11 +2938,6 @@ "json-bigint": "^0.3.0" } }, - "generic-pool": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.5.0.tgz", - "integrity": "sha512-dEkxmX+egB2o4NR80c/q+xzLLzLX+k68/K8xv81XprD+Sk7ZtP14VugeCz+fUwv5FzpWq40pPtAkzPRqT8ka9w==" - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4107,6 +4102,14 @@ "minipass": "^2.2.1" } }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, "mocha": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.0.tgz", @@ -4421,16 +4424,6 @@ "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - } } }, "nopt": { @@ -5803,12 +5796,11 @@ } }, "retry-as-promised": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.3.2.tgz", - "integrity": "sha1-zZdO5P2bX+A8vzGHHuSCIcB3N7c=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", + "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==", "requires": { - "bluebird": "^3.4.6", - "debug": "^2.6.9" + "any-promise": "^1.3.0" } }, "retry-axios": { @@ -5938,44 +5930,57 @@ } }, "sequelize": { - "version": "4.44.4", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.44.4.tgz", - "integrity": "sha512-nkHmYkbwQK7uwpgW9VBalCBnQqQ8mslTdgcBthtJLORuPvAYRPlfkXZMVUU9TLLJt9CX+/y0MYg0DpcP6ywsEQ==", + "version": "5.21.5", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.21.5.tgz", + "integrity": "sha512-n9hR5K4uQGmBGK/Y/iqewCeSFmKVsd0TRnh0tfoLoAkmXbKC4tpeK96RhKs7d+TTMtrJlgt2TNLVBaAxEwC4iw==", "requires": { "bluebird": "^3.5.0", "cls-bluebird": "^2.1.0", - "debug": "^3.1.0", - "depd": "^1.1.0", + "debug": "^4.1.1", "dottie": "^2.0.0", - "generic-pool": "3.5.0", "inflection": "1.12.0", - "lodash": "^4.17.1", - "moment": "^2.20.0", - "moment-timezone": "^0.5.14", - "retry-as-promised": "^2.3.2", - "semver": "^5.5.0", - "terraformer-wkt-parser": "^1.1.2", + "lodash": "^4.17.15", + "moment": "^2.24.0", + "moment-timezone": "^0.5.21", + "retry-as-promised": "^3.2.0", + "semver": "^6.3.0", + "sequelize-pool": "^2.3.0", "toposort-class": "^1.0.1", - "uuid": "^3.2.1", - "validator": "^10.4.0", - "wkx": "^0.4.1" + "uuid": "^3.3.3", + "validator": "^10.11.0", + "wkx": "^0.4.8" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, + "sequelize-pool": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", + "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==" + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -6564,23 +6569,6 @@ "uuid": "^3.3.2" } }, - "terraformer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.10.tgz", - "integrity": "sha512-5c6cAfKTZHAeRdT8sIRRidhN1w+vsmf3RmQn+PKksFhTUnsBtjQdbJG2vaxM6T47IU2EeR1S8t8UjTYY9Q1yJA==", - "requires": { - "@types/geojson": "^7946.0.0 || ^1.0.0" - } - }, - "terraformer-wkt-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.1.tgz", - "integrity": "sha512-+CJyNLWb3lJ9RsZMTM66BY0MT3yIo4l4l22Jd9CrZuwzk54fsu4Sc7zejuS9fCITTuTQy3p06d4MZMVI7v5wSg==", - "requires": { - "@types/geojson": "^1.0.0", - "terraformer": "~1.0.5" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index bdbbd5ff..6760a47d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "metrics-sharelatex": "^2.5.1", "mysql": "^2.18.1", "request": "^2.88.2", - "sequelize": "^4.44.4", + "sequelize": "^5.21.5", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", "sqlite3": "^4.1.1", From 4a47f21edd87de5bd2b90fe884a12cc165be8bff Mon Sep 17 00:00:00 2001 From: mserranom <mserranom@gmail.com> Date: Mon, 16 Mar 2020 17:14:04 +0100 Subject: [PATCH 509/709] updated sandboxed-module, chai and metrics-sharelatex --- package-lock.json | 87 ++++++++++++----------- package.json | 6 +- test/unit/js/ResourceStateManagerTests.js | 1 + test/unit/js/ResourceWriterTests.js | 1 + 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index dcb2e946..9167d627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1227,9 +1227,9 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assertion-error": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, "ast-types-flow": { @@ -1487,13 +1487,17 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chai": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-1.8.1.tgz", - "integrity": "sha1-zHeGbV5+vKK9dRRLHtw3Coh4X3I=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", "dev": true, "requires": { - "assertion-error": "1.0.0", - "deep-eql": "0.1.3" + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" } }, "chalk": { @@ -1530,6 +1534,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -1810,12 +1820,12 @@ "dev": true }, "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { - "type-detect": "0.1.1" + "type-detect": "^4.0.0" } }, "deep-extend": { @@ -2944,6 +2954,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", @@ -4018,9 +4034,9 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "metrics-sharelatex": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.5.1.tgz", - "integrity": "sha512-C2gmkl/tUnq3IlSX/x3dixGhdvfD6H9FR9mBf9lnkeyy2arafxhCU6u+1IQj6byjBM7vGpYHyjwWnmoi3Vb+ZQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.6.0.tgz", + "integrity": "sha512-kPWCtgBrRZwLXCxqJVVn3c7g+GHQEBGYBpwCIt0Vqp0NaKvgKiPkJMkoPg9vkCsjsN2AgpGxXcOAdnHAjxfrzA==", "requires": { "@google-cloud/debug-agent": "^3.0.0", "@google-cloud/profiler": "^0.2.3", @@ -4314,9 +4330,9 @@ } }, "nan": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", - "integrity": "sha1-nUQ/214TogdwzF5gLu5ZdgpoWIU=" + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "natural-compare": { "version": "1.4.0", @@ -4743,6 +4759,12 @@ } } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -5875,21 +5897,13 @@ "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" }, "sandboxed-module": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.3.0.tgz", - "integrity": "sha1-8fvvvYCaT2kHO9B8rm/H2y6vX2o=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-2.0.3.tgz", + "integrity": "sha1-x+VFkzm7y6KMUwPusz9ug4e/upY=", "dev": true, "requires": { "require-like": "0.1.2", - "stack-trace": "0.0.6" - }, - "dependencies": { - "stack-trace": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", - "integrity": "sha1-HnGb1qJin/CcGJ4Xqe+QKpT8XbA=", - "dev": true - } + "stack-trace": "0.0.9" } }, "sax": { @@ -6238,13 +6252,6 @@ "nan": "^2.12.1", "node-pre-gyp": "^0.11.0", "request": "^2.87.0" - }, - "dependencies": { - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - } } }, "sqlstring": { @@ -6694,9 +6701,9 @@ } }, "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, "type-fest": { diff --git a/package.json b/package.json index 6760a47d..ed0db3e3 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "lockfile": "^1.0.4", "logger-sharelatex": "^1.9.0", "lynx": "0.2.0", - "metrics-sharelatex": "^2.5.1", + "metrics-sharelatex": "^2.6.0", "mysql": "^2.18.1", "request": "^2.88.2", "sequelize": "^5.21.5", @@ -42,7 +42,7 @@ "devDependencies": { "babel-eslint": "^10.1.0", "bunyan": "^1.8.12", - "chai": "~1.8.1", + "chai": "~4.2.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.0", "eslint-config-standard": "^14.1.0", @@ -61,7 +61,7 @@ "mocha": "^7.1.0", "prettier": "^1.19.1", "prettier-eslint-cli": "^5.0.0", - "sandboxed-module": "~0.3.0", + "sandboxed-module": "^2.0.3", "sinon": "~9.0.1", "timekeeper": "2.2.0" } diff --git a/test/unit/js/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js index 4c16ed1f..efc4065b 100644 --- a/test/unit/js/ResourceStateManagerTests.js +++ b/test/unit/js/ResourceStateManagerTests.js @@ -23,6 +23,7 @@ const Errors = require('../../../app/js/Errors') describe('ResourceStateManager', function() { beforeEach(function() { this.ResourceStateManager = SandboxedModule.require(modulePath, { + singleOnly: true, requires: { fs: (this.fs = {}), 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 2844ce71..68aa4560 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -24,6 +24,7 @@ describe('ResourceWriter', function() { beforeEach(function() { let Timer this.ResourceWriter = SandboxedModule.require(modulePath, { + singleOnly: true, requires: { fs: (this.fs = { mkdir: sinon.stub().callsArg(1), From 2e5e040475518753606a1ff8945a949cac2ebac6 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Fri, 20 Mar 2020 13:37:58 +0000 Subject: [PATCH 510/709] limit clsi lifespan via health checks and PROCESS_LIFE_SPAN_LIMIT_MS --- app.js | 44 ++++++++++++++++--------------------- config/settings.defaults.js | 9 ++++---- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app.js b/app.js index c03fcd8b..ec67140c 100644 --- a/app.js +++ b/app.js @@ -22,7 +22,6 @@ const ContentTypeMapper = require('./app/js/ContentTypeMapper') const Errors = require('./app/js/Errors') const Path = require('path') -const fs = require('fs') Metrics.open_sockets.monitor(logger) Metrics.memory.monitor(logger) @@ -208,23 +207,35 @@ const resCacher = { setContentType: 'application/json' } +const startupTime = Date.now() +const checkIfProcessIsTooOld = function() { + if ( + Settings.processLifespanLimitMs && + startupTime + Settings.processLifespanLimitMs < Date.now() + ) { + logger.log('shutting down, process is too old') + resCacher.send = function() {} + resCacher.statusCode = 500 + resCacher.body = { processToOld: true } + } +} + if (Settings.smokeTest) { - let runSmokeTest - ;(runSmokeTest = function() { + const runSmokeTest = function() { + checkIfProcessIsTooOld() logger.log('running smoke tests') smokeTest.run(require.resolve(__dirname + '/test/smoke/js/SmokeTests.js'))( {}, resCacher ) return setTimeout(runSmokeTest, 30 * 1000) - })() + } + runSmokeTest() } app.get('/health_check', function(req, res) { - res.contentType(resCacher != null ? resCacher.setContentType : undefined) - return res - .status(resCacher != null ? resCacher.code : undefined) - .send(resCacher != null ? resCacher.body : undefined) + res.contentType(resCacher.setContentType) + return res.status(resCacher.code).send(resCacher.body) }) app.get('/smoke_test_force', (req, res) => @@ -234,23 +245,6 @@ app.get('/smoke_test_force', (req, res) => ) ) -const profiler = require('v8-profiler-node8') -app.get('/profile', function(req, res) { - const time = parseInt(req.query.time || '1000') - profiler.startProfiling('test') - return setTimeout(function() { - const profile = profiler.stopProfiling('test') - return res.json(profile) - }, time) -}) - -app.get('/heapdump', (req, res) => - require('heapdump').writeSnapshot( - `/tmp/${Date.now()}.clsi.heapsnapshot`, - (err, filename) => res.send(filename) - ) -) - app.use(function(error, req, res, next) { if (error instanceof Errors.NotFoundError) { logger.warn({ err: error, url: req.url }, 'not found error') diff --git a/config/settings.defaults.js b/config/settings.defaults.js index b0fd0cbd..fb7384bb 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -22,6 +22,9 @@ module.exports = { compileSizeLimit: process.env.COMPILE_SIZE_LIMIT || '7mb', + processLifespanLimitMs: + process.env.PROCESS_LIFE_SPAN_LIMIT_MS || 60 * 60 * 24 * 1000 * 2, + path: { compilesDir: Path.resolve(__dirname + '/../compiles'), clsiCacheDir: Path.resolve(__dirname + '/../cache'), @@ -65,8 +68,7 @@ if (process.env.DOCKER_RUNNER) { dockerRunner: process.env.DOCKER_RUNNER === 'true', docker: { image: - process.env.TEXLIVE_IMAGE || - 'quay.io/sharelatex/texlive-full:2017.1', + process.env.TEXLIVE_IMAGE || 'quay.io/sharelatex/texlive-full:2017.1', env: { HOME: '/tmp' }, @@ -93,8 +95,7 @@ if (process.env.DOCKER_RUNNER) { module.exports.path.synctexBaseDir = () => '/compile' - module.exports.path.sandboxedCompilesHostDir = - process.env.COMPILES_HOST_DIR + module.exports.path.sandboxedCompilesHostDir = process.env.COMPILES_HOST_DIR module.exports.path.synctexBinHostPath = process.env.SYNCTEX_BIN_HOST_PATH } From 96a41a5f17440e5d0fe31d0fc924361aeddcd157 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Mon, 23 Mar 2020 16:18:07 +0100 Subject: [PATCH 511/709] [misc] bump logger-sharelatex to 1.9.1 --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9167d627..c64e7d44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3816,9 +3816,9 @@ } }, "logger-sharelatex": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.9.0.tgz", - "integrity": "sha512-yVTuha82047IiMOQLgQHCZGKkJo6I2+2KtiFKpgkIooR2yZaoTEvAeoMwBesSDSpGUpvUJ/+9UI+PmRyc+PQKQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.9.1.tgz", + "integrity": "sha512-9s6JQnH/PN+Js2CmI8+J3MQCTNlRzP2Dh4pcekXrV6Jm5J4HzyPi+6d3zfBskZ4NBmaUVw9hC4p5dmdaRmh4mQ==", "requires": { "@google-cloud/logging-bunyan": "^2.0.0", "@overleaf/o-error": "^2.0.0", diff --git a/package.json b/package.json index ed0db3e3..00440a8b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "fs-extra": "^8.1.0", "heapdump": "^0.3.15", "lockfile": "^1.0.4", - "logger-sharelatex": "^1.9.0", + "logger-sharelatex": "^1.9.1", "lynx": "0.2.0", "metrics-sharelatex": "^2.6.0", "mysql": "^2.18.1", From 4a26ec975f082971b023e90c9d501b530285925f Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Fri, 27 Mar 2020 10:39:45 +0100 Subject: [PATCH 512/709] [misc] keep up with the error signature of dockerode/docker-modem https://github.com/apocas/docker-modem/blob/v2.1.1/lib/modem.js#L296 --- app/js/DockerRunner.js | 6 +----- test/unit/js/DockerRunnerTests.js | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 393ce3df..3594b3a6 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -99,11 +99,7 @@ module.exports = DockerRunner = { error, output ) { - if ( - __guard__(error != null ? error.message : undefined, x => - x.match('HTTP code is 500') - ) - ) { + if (error && error.statusCode === 500) { logger.log( { err: error, project_id }, 'error running container so destroying and retrying' diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index eea66b12..83992833 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -202,9 +202,9 @@ describe('DockerRunner', function() { } if (firstTime) { firstTime = false - return callback( - new Error('HTTP code is 500 which indicates error: server error') - ) + const error = new Error('(HTTP code 500) server error - ...') + error.statusCode = 500 + return callback(error) } else { return callback(null, this.output) } From 00c5ace592ec6d6d42ffccaa433ce4e2af69f773 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Fri, 3 Apr 2020 12:18:09 +0200 Subject: [PATCH 513/709] [misc] bump the build-scripts to version 2.1.0 This will put acceptance and unit tests in own namespaces so that they can run and be teared down individually. --- Makefile | 42 ++++++++++++++++++++++++++++++++++-------- buildscript.txt | 2 +- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 0f1d274f..c938f87a 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) PROJECT_NAME = clsi +BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') + DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ BRANCH_NAME=$(BRANCH_NAME) \ @@ -12,6 +14,12 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ MOCHA_GREP=${MOCHA_GREP} \ docker-compose ${DOCKER_COMPOSE_FLAGS} +DOCKER_COMPOSE_TEST_ACCEPTANCE = \ + COMPOSE_PROJECT_NAME=test_acceptance_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) + +DOCKER_COMPOSE_TEST_UNIT = \ + COMPOSE_PROJECT_NAME=test_unit_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) + clean: docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) @@ -28,23 +36,41 @@ lint: test: format lint test_unit test_acceptance test_unit: - @[ ! -d test/unit ] && echo "clsi has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit + $(MAKE) test_unit_clean +endif + +test_clean: test_unit_clean +test_unit_clean: +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) down -v -t 0 +endif -test_acceptance: test_clean test_acceptance_pre_run test_acceptance_run +test_acceptance: test_acceptance_clean test_acceptance_pre_run test_acceptance_run + $(MAKE) test_acceptance_clean -test_acceptance_debug: test_clean test_acceptance_pre_run test_acceptance_run_debug +test_acceptance_debug: test_acceptance_clean test_acceptance_pre_run test_acceptance_run_debug + $(MAKE) test_acceptance_clean test_acceptance_run: - @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance +endif test_acceptance_run_debug: - @[ ! -d test/acceptance ] && echo "clsi has no acceptance tests" || $(DOCKER_COMPOSE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk +endif -test_clean: - $(DOCKER_COMPOSE) down -v -t 0 +test_clean: test_acceptance_clean +test_acceptance_clean: + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) down -v -t 0 test_acceptance_pre_run: - @[ ! -f test/acceptance/js/scripts/pre-run ] && echo "clsi has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/js/scripts/pre-run +ifneq (,$(wildcard test/acceptance/js/scripts/pre-run)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance test/acceptance/js/scripts/pre-run +endif build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ diff --git a/buildscript.txt b/buildscript.txt index 52686425..1f089349 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -7,4 +7,4 @@ clsi --language=es --node-version=10.19.0 --public-repo=True ---script-version=2.0.0 +--script-version=2.1.0 From 638d2f30d3f7f3a78b1bbf586fdb8398428e4c04 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Thu, 26 Mar 2020 11:18:50 +0100 Subject: [PATCH 514/709] [misc] add a metric for failing downloads --- app/js/ResourceWriter.js | 1 + test/unit/js/ResourceWriterTests.js | 1 + 2 files changed, 2 insertions(+) diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index 5a234e0d..ed3a1e87 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -324,6 +324,7 @@ module.exports = ResourceWriter = { }, 'error downloading file for resources' ) + Metrics.inc('download-failed') } return callback() } diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 68aa4560..a632c1bd 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -36,6 +36,7 @@ describe('ResourceWriter', function() { './OutputFileFinder': (this.OutputFileFinder = {}), 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, './Metrics': (this.Metrics = { + inc: sinon.stub(), Timer: (Timer = (function() { Timer = class Timer { static initClass() { From b69ea2f83f0e052bbc160c0a3a9689447f7da3f2 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Wed, 19 Feb 2020 10:32:50 +0000 Subject: [PATCH 515/709] [misc] drop debug output and log docker version on stderr --- entrypoint.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 07d902ac..e28bbe66 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,9 +1,6 @@ #!/bin/sh -echo "Changing permissions of /var/run/docker.sock for sibling containers" -ls -al /var/run/docker.sock -docker --version -cat /etc/passwd +docker --version >&2 DOCKER_GROUP=$(stat -c '%g' /var/run/docker.sock) groupadd --non-unique --gid ${DOCKER_GROUP} dockeronhost From c5d10d02fc83ed0453cffcbbd635e9747b4096f3 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Wed, 19 Feb 2020 12:23:20 +0100 Subject: [PATCH 516/709] [misc] move the sqlite database into a db/ directory --- .dockerignore | 1 + Dockerfile | 2 ++ buildscript.txt | 1 + config/settings.defaults.js | 8 +++----- db/.gitignore | 2 ++ 5 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 db/.gitignore diff --git a/.dockerignore b/.dockerignore index ba1c3442..35f8905e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ gitrev .npm .nvmrc nodemon.json +db/ diff --git a/Dockerfile b/Dockerfile index 27158b5d..3fbae08b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,5 +24,7 @@ COPY . /app FROM base COPY --from=app /app /app +RUN mkdir -p db \ +&& chown node:node db CMD ["node", "--expose-gc", "app.js"] diff --git a/buildscript.txt b/buildscript.txt index 1f089349..72b0f6a3 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,5 +1,6 @@ clsi --acceptance-creds=None +--data-dirs=db --dependencies= --docker-repos=gcr.io/overleaf-ops --env-add= diff --git a/config/settings.defaults.js b/config/settings.defaults.js index b0fd0cbd..021c9cd7 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -9,7 +9,7 @@ module.exports = { username: 'clsi', dialect: 'sqlite', storage: - process.env.SQLITE_PATH || Path.resolve(__dirname + '/../db.sqlite'), + process.env.SQLITE_PATH || Path.resolve(__dirname + '/../db/db.sqlite'), pool: { max: 1, min: 1 @@ -65,8 +65,7 @@ if (process.env.DOCKER_RUNNER) { dockerRunner: process.env.DOCKER_RUNNER === 'true', docker: { image: - process.env.TEXLIVE_IMAGE || - 'quay.io/sharelatex/texlive-full:2017.1', + process.env.TEXLIVE_IMAGE || 'quay.io/sharelatex/texlive-full:2017.1', env: { HOME: '/tmp' }, @@ -93,8 +92,7 @@ if (process.env.DOCKER_RUNNER) { module.exports.path.synctexBaseDir = () => '/compile' - module.exports.path.sandboxedCompilesHostDir = - process.env.COMPILES_HOST_DIR + module.exports.path.sandboxedCompilesHostDir = process.env.COMPILES_HOST_DIR module.exports.path.synctexBinHostPath = process.env.SYNCTEX_BIN_HOST_PATH } diff --git a/db/.gitignore b/db/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/db/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From 4ee0dc2471d065053fe668718c2517e545600710 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Wed, 19 Feb 2020 12:06:28 +0100 Subject: [PATCH 517/709] [misc] narrow down write access/ownership for the run-time user --- .dockerignore | 2 ++ Dockerfile | 4 ++-- buildscript.txt | 2 +- entrypoint.sh | 17 ++++++++--------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index 35f8905e..74fdc35e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,6 @@ gitrev .npm .nvmrc nodemon.json +cache/ +compiles/ db/ diff --git a/Dockerfile b/Dockerfile index 3fbae08b..40615ad8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ COPY . /app FROM base COPY --from=app /app /app -RUN mkdir -p db \ -&& chown node:node db +RUN mkdir -p cache compiles db \ +&& chown node:node cache compiles db CMD ["node", "--expose-gc", "app.js"] diff --git a/buildscript.txt b/buildscript.txt index 72b0f6a3..81d65464 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,6 +1,6 @@ clsi --acceptance-creds=None ---data-dirs=db +--data-dirs=cache,compiles,db --dependencies= --docker-repos=gcr.io/overleaf-ops --env-add= diff --git a/entrypoint.sh b/entrypoint.sh index e28bbe66..3e3f8382 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,22 +2,21 @@ docker --version >&2 +# add the node user to the docker group on the host DOCKER_GROUP=$(stat -c '%g' /var/run/docker.sock) groupadd --non-unique --gid ${DOCKER_GROUP} dockeronhost usermod -aG dockeronhost node -mkdir -p /app/cache -chown -R node:node /app/cache +# compatibility: initial volume setup +chown node:node /app/cache +chown node:node /app/compiles +chown node:node /app/db -mkdir -p /app/compiles -chown -R node:node /app/compiles - -chown -R node:node /app/bin/synctex +# acceptance tests mkdir -p /app/test/acceptance/fixtures/tmp/ -chown -R node:node /app - -chown -R node:node /app/bin +chown -R node:node /app/test/acceptance/fixtures +# make synctex available for remount in compiles cp /app/bin/synctex /app/bin/synctex-mount/synctex exec runuser -u node -- "$@" From 3db40804baaf30306f966e2cf506f594fa81de5b Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Fri, 27 Mar 2020 11:10:27 +0100 Subject: [PATCH 518/709] [misc] use a directory in /tmp for temporary data --- entrypoint.sh | 4 --- test/acceptance/js/ExampleDocumentTests.js | 29 +++++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 3e3f8382..26965748 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -12,10 +12,6 @@ chown node:node /app/cache chown node:node /app/compiles chown node:node /app/db -# acceptance tests -mkdir -p /app/test/acceptance/fixtures/tmp/ -chown -R node:node /app/test/acceptance/fixtures - # make synctex available for remount in compiles cp /app/bin/synctex /app/bin/synctex-mount/synctex diff --git a/test/acceptance/js/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js index 110b5d6f..0134c0e1 100644 --- a/test/acceptance/js/ExampleDocumentTests.js +++ b/test/acceptance/js/ExampleDocumentTests.js @@ -19,11 +19,17 @@ const Client = require('./helpers/Client') const request = require('request') require('chai').should() const fs = require('fs') +const fsExtra = require('fs-extra') const ChildProcess = require('child_process') const ClsiApp = require('./helpers/ClsiApp') const logger = require('logger-sharelatex') const Path = require('path') -const fixturePath = path => Path.normalize(__dirname + '/../fixtures/' + path) +const fixturePath = path => { + if (path.slice(0, 3) === 'tmp') { + return '/tmp/clsi_acceptance_tests' + path.slice(3) + } + return Path.normalize(__dirname + '/../fixtures/' + path) +} const process = require('process') console.log( process.pid, @@ -32,13 +38,6 @@ console.log( process.getgroups(), 'PID' ) -try { - console.log('creating tmp directory', fixturePath('tmp')) - fs.mkdirSync(fixturePath('tmp')) -} catch (error) { - const err = error - console.log(err, fixturePath('tmp'), 'unable to create fixture tmp path') -} const MOCHA_LATEX_TIMEOUT = 60 * 1000 @@ -201,10 +200,16 @@ Client.runServer(4242, fixturePath('examples')) describe('Example Documents', function() { before(function(done) { - return ChildProcess.exec('rm test/acceptance/fixtures/tmp/*').on( - 'exit', - () => ClsiApp.ensureRunning(done) - ) + ClsiApp.ensureRunning(done) + }) + before(function(done) { + fsExtra.remove(fixturePath('tmp'), done) + }) + before(function(done) { + fs.mkdir(fixturePath('tmp'), done) + }) + after(function(done) { + fsExtra.remove(fixturePath('tmp'), done) }) return Array.from(fs.readdirSync(fixturePath('examples'))).map(example_dir => From 8fa4232148ebc0fa89bbfe7689a0f41c52e32b50 Mon Sep 17 00:00:00 2001 From: Tim Alby <timothee.alby@gmail.com> Date: Fri, 10 Apr 2020 12:25:55 +0200 Subject: [PATCH 519/709] fix arguments order Co-Authored-By: Jakob Ackermann <jakob.ackermann@overleaf.com> --- test/unit/js/DockerRunnerTests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 83992833..3daded89 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -357,8 +357,8 @@ describe('DockerRunner', function() { return this.DockerRunner.startContainer( this.options, this.volumes, - this.callback, - () => {} + () => {}, + this.callback ) }) From e3da458b376871c3ce72d6984d14bf1ee668b04b Mon Sep 17 00:00:00 2001 From: Tim Alby <timothee.alby@gmail.com> Date: Fri, 10 Apr 2020 12:28:11 +0200 Subject: [PATCH 520/709] retry once on EPIPE errors Co-Authored-By: Jakob Ackermann <jakob.ackermann@overleaf.com> --- app/js/DockerRunner.js | 34 +++++++++++------- test/unit/js/DockerRunnerTests.js | 58 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 3594b3a6..32bcf707 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -407,19 +407,29 @@ module.exports = DockerRunner = { }) } ) - return container.inspect(function(error, stats) { - if ((error != null ? error.statusCode : undefined) === 404) { - return createAndStartContainer() - } else if (error != null) { - logger.err( - { container_name: name, error }, - 'unable to inspect container to start' - ) - return callback(error) - } else { - return startExistingContainer() + + const callbackWithRetry = error => { + if (error.message.match(/EPIPE/)) { + return inspectContainer(container, callback) } - }) + callback(error) + } + + var inspectContainer = (container, innerCallback) => + container.inspect(function(error, stats) { + if ((error != null ? error.statusCode : undefined) === 404) { + return createAndStartContainer() + } else if (error != null) { + logger.err( + { container_name: name, error }, + 'unable to inspect container to start' + ) + return innerCallback(error) + } else { + return startExistingContainer() + } + }) + inspectContainer(container, callbackWithRetry) }, attachToContainer(containerId, attachStreamHandler, attachStartCallback) { diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 3daded89..7284c6e5 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -36,6 +36,7 @@ describe('DockerRunner', function() { 'logger-sharelatex': (this.logger = { log: sinon.stub(), error: sinon.stub(), + err: sinon.stub(), info: sinon.stub(), warn: sinon.stub() }), @@ -387,6 +388,63 @@ describe('DockerRunner', function() { }) }) + describe('when inspect always fails with EPIPE error', function() { + beforeEach(function() { + this.error = new Error('write EPIPE') + this.container.inspect = sinon.stub().yields(this.error) + this.container.start = sinon.stub().yields() + + this.DockerRunner.startContainer( + this.options, + this.volumes, + () => {}, + this.callback + ) + }) + + it('should retry once', function() { + sinon.assert.callOrder( + this.container.inspect, + this.container.inspect, + this.callback + ) + }) + + it('should call back with error', function() { + sinon.assert.calledWith(this.callback, this.error) + }) + }) + + describe('when inspect fails once with EPIPE error', function() { + beforeEach(function() { + this.container.inspect = sinon.stub() + this.container.inspect.onFirstCall().yields(new Error('write EPIPE')) + this.container.inspect.onSecondCall().yields() + this.container.start = sinon.stub().yields() + + this.DockerRunner.startContainer( + this.options, + this.volumes, + () => {}, + this.callback + ) + }) + + it('should retry once and start container', function() { + sinon.assert.callOrder( + this.container.inspect, + this.container.inspect, + this.DockerRunner.attachToContainer, + this.container.start, + this.callback + ) + }) + + it('should call back without error', function() { + sinon.assert.calledWith(this.callback, null) + }) + }) + describe('when the container does not exist', function() { beforeEach(function() { const exists = false From 9e82ab0890c5cc8c7fb95362c3f7edbcaad0cf29 Mon Sep 17 00:00:00 2001 From: Tim Alby <timothee.alby@gmail.com> Date: Fri, 10 Apr 2020 12:28:48 +0200 Subject: [PATCH 521/709] add metrics for EPIPE errors Co-Authored-By: Jakob Ackermann <jakob.ackermann@overleaf.com> --- app/js/DockerRunner.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 32bcf707..552f65ea 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -26,6 +26,7 @@ const LockManager = require('./DockerLockManager') const fs = require('fs') const Path = require('path') const _ = require('underscore') +const metrics = require('metrics-sharelatex') logger.info('using docker runner') @@ -410,6 +411,7 @@ module.exports = DockerRunner = { const callbackWithRetry = error => { if (error.message.match(/EPIPE/)) { + metrics.inc('container-inspect-epipe-retry') return inspectContainer(container, callback) } callback(error) @@ -420,6 +422,7 @@ module.exports = DockerRunner = { if ((error != null ? error.statusCode : undefined) === 404) { return createAndStartContainer() } else if (error != null) { + metrics.inc('container-inspect-epipe-error') logger.err( { container_name: name, error }, 'unable to inspect container to start' From 2b2fcca39ce8dee0fdc0c342aa0d6c822592bcec Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Fri, 10 Apr 2020 13:25:40 +0200 Subject: [PATCH 522/709] [DockerRunner] fix metric incrementing and error logging - do not log on first EPIPE - inc 'container-inspect-epipe-error' on permanent error only Co-Authored-By: Tim Alby <timothee.alby@gmail.com> --- app/js/DockerRunner.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 552f65ea..6a603d8f 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -408,31 +408,28 @@ module.exports = DockerRunner = { }) } ) - - const callbackWithRetry = error => { - if (error.message.match(/EPIPE/)) { - metrics.inc('container-inspect-epipe-retry') - return inspectContainer(container, callback) - } - callback(error) - } - - var inspectContainer = (container, innerCallback) => + var inspectContainer = (isRetry) => container.inspect(function(error, stats) { if ((error != null ? error.statusCode : undefined) === 404) { return createAndStartContainer() } else if (error != null) { - metrics.inc('container-inspect-epipe-error') + if (error.message.match(/EPIPE/)) { + if (!isRetry) { + metrics.inc('container-inspect-epipe-retry') + return inspectContainer(true) + } + metrics.inc('container-inspect-epipe-error') + } logger.err( { container_name: name, error }, 'unable to inspect container to start' ) - return innerCallback(error) + return callback(error) } else { return startExistingContainer() } }) - inspectContainer(container, callbackWithRetry) + inspectContainer(false) }, attachToContainer(containerId, attachStreamHandler, attachStartCallback) { From 3513748f732e5eb7b0408c3055412c238497fb8b Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Thu, 9 Apr 2020 14:11:04 +0100 Subject: [PATCH 523/709] add variance into shutdown time to avoid stampeed --- app.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/app.js b/app.js index ec67140c..f96d019f 100644 --- a/app.js +++ b/app.js @@ -208,27 +208,34 @@ const resCacher = { } const startupTime = Date.now() -const checkIfProcessIsTooOld = function() { +const checkIfProcessIsTooOld = function(cont) { + if (typeof Settings.processLifespanLimitMs === 'string') { + Settings.processLifespanLimitMs = parseInt(Settings.processLifespanLimitMs) + Settings.processLifespanLimitMs += + Settings.processLifespanLimitMs * (Math.random() / 10) + } if ( Settings.processLifespanLimitMs && startupTime + Settings.processLifespanLimitMs < Date.now() ) { logger.log('shutting down, process is too old') resCacher.send = function() {} - resCacher.statusCode = 500 + resCacher.code = 500 resCacher.body = { processToOld: true } + } else { + cont() } } if (Settings.smokeTest) { const runSmokeTest = function() { - checkIfProcessIsTooOld() - logger.log('running smoke tests') - smokeTest.run(require.resolve(__dirname + '/test/smoke/js/SmokeTests.js'))( - {}, - resCacher - ) - return setTimeout(runSmokeTest, 30 * 1000) + checkIfProcessIsTooOld(function() { + logger.log('running smoke tests') + smokeTest.run( + require.resolve(__dirname + '/test/smoke/js/SmokeTests.js') + )({}, resCacher) + return setTimeout(runSmokeTest, 30 * 1000) + }) } runSmokeTest() } From b18c9854b6c03f70cddd3556b7425de57bee1b5a Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Thu, 19 Sep 2019 00:44:02 +0200 Subject: [PATCH 524/709] [LocalCommandRunner] run: block a double call of the callback The subprocess event handler fires the "error" and "close" event in case of a failure. Both events would call the given callback, resulting in double processing of the subprocess result downstream. Signed-off-by: Jakob Ackermann <das7pad@outlook.com> --- app/js/LocalCommandRunner.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/js/LocalCommandRunner.js b/app/js/LocalCommandRunner.js index 61ecd887..67f1a33c 100644 --- a/app/js/LocalCommandRunner.js +++ b/app/js/LocalCommandRunner.js @@ -15,6 +15,7 @@ */ let CommandRunner const { spawn } = require('child_process') +const _ = require('underscore') const logger = require('logger-sharelatex') logger.info('using standard command runner') @@ -24,6 +25,8 @@ module.exports = CommandRunner = { let key, value if (callback == null) { callback = function(error) {} + } else { + callback = _.once(callback) } command = Array.from(command).map(arg => arg.toString().replace('$COMPILE_DIR', directory) From 557dc47e30c4cbcc8017a761c37dfae0aada2e61 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@gmail.com> Date: Thu, 23 Apr 2020 11:32:33 +0100 Subject: [PATCH 525/709] cleanup the shutdown code a bit --- app.js | 19 +++++++++---------- config/settings.defaults.js | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app.js b/app.js index f96d019f..80b00ef5 100644 --- a/app.js +++ b/app.js @@ -207,17 +207,16 @@ const resCacher = { setContentType: 'application/json' } -const startupTime = Date.now() +let shutdownTime +if (Settings.processLifespanLimitMs) { + Settings.processLifespanLimitMs += + Settings.processLifespanLimitMs * (Math.random() / 10) + shutdownTime = Date.now() + Settings.processLifespanLimitMs + logger.info('Lifespan limited to ', shutdownTime) +} + const checkIfProcessIsTooOld = function(cont) { - if (typeof Settings.processLifespanLimitMs === 'string') { - Settings.processLifespanLimitMs = parseInt(Settings.processLifespanLimitMs) - Settings.processLifespanLimitMs += - Settings.processLifespanLimitMs * (Math.random() / 10) - } - if ( - Settings.processLifespanLimitMs && - startupTime + Settings.processLifespanLimitMs < Date.now() - ) { + if (shutdownTime && shutdownTime < Date.now()) { logger.log('shutting down, process is too old') resCacher.send = function() {} resCacher.code = 500 diff --git a/config/settings.defaults.js b/config/settings.defaults.js index fb7384bb..e5df0624 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -23,7 +23,7 @@ module.exports = { compileSizeLimit: process.env.COMPILE_SIZE_LIMIT || '7mb', processLifespanLimitMs: - process.env.PROCESS_LIFE_SPAN_LIMIT_MS || 60 * 60 * 24 * 1000 * 2, + parseInt(process.env.PROCESS_LIFE_SPAN_LIMIT_MS) || 60 * 60 * 24 * 1000 * 2, path: { compilesDir: Path.resolve(__dirname + '/../compiles'), From 5b5fd2f5dff3c64c2adb83f68be0d6371cd35b3a Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 7 May 2020 10:30:14 +0100 Subject: [PATCH 526/709] set encoding when reading from streams using .toString() works most of the time but can lead to utf8 characters being broken across chunk boundaries. https://nodejs.org/api/stream.html#stream_readable_setencoding_encoding --- app/js/CompileManager.js | 2 +- app/js/LocalCommandRunner.js | 2 +- app/js/OutputFileFinder.js | 2 +- app/js/OutputFileOptimiser.js | 2 +- test/unit/js/CompileManagerTests.js | 2 ++ test/unit/js/OutputFileFinderTests.js | 1 + 6 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 3bf54bc7..dd62f435 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -334,7 +334,7 @@ module.exports = CompileManager = { proc.on('error', callback) let stderr = '' - proc.stderr.on('data', chunk => (stderr += chunk.toString())) + proc.stderr.setEncoding('utf8').on('data', chunk => (stderr += chunk)) return proc.on('close', function(code) { if (code === 0) { diff --git a/app/js/LocalCommandRunner.js b/app/js/LocalCommandRunner.js index 61ecd887..ccaf5078 100644 --- a/app/js/LocalCommandRunner.js +++ b/app/js/LocalCommandRunner.js @@ -46,7 +46,7 @@ module.exports = CommandRunner = { const proc = spawn(command[0], command.slice(1), { cwd: directory, env }) let stdout = '' - proc.stdout.on('data', data => (stdout += data)) + proc.stdout.setEncoding('utf8').on('data', data => (stdout += data)) proc.on('error', function(err) { logger.err( diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 50012b51..83199781 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -87,7 +87,7 @@ module.exports = OutputFileFinder = { const proc = spawn('find', args) let stdout = '' - proc.stdout.on('data', chunk => (stdout += chunk.toString())) + proc.stdout.setEncoding('utf8').on('data', chunk => (stdout += chunk)) proc.on('error', callback) return proc.on('close', function(code) { if (code !== 0) { diff --git a/app/js/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js index c0b8cc14..41ba7b40 100644 --- a/app/js/OutputFileOptimiser.js +++ b/app/js/OutputFileOptimiser.js @@ -78,7 +78,7 @@ module.exports = OutputFileOptimiser = { const timer = new Metrics.Timer('qpdf') const proc = spawn('qpdf', args) let stdout = '' - proc.stdout.on('data', chunk => (stdout += chunk.toString())) + proc.stdout.setEncoding('utf8').on('data', chunk => (stdout += chunk)) callback = _.once(callback) // avoid double call back for error and close event proc.on('error', function(err) { logger.warn({ err, args }, 'qpdf failed') diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index 180f6f31..74a0a47f 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -294,6 +294,7 @@ describe('CompileManager', function() { this.proc = new EventEmitter() this.proc.stdout = new EventEmitter() this.proc.stderr = new EventEmitter() + this.proc.stderr.setEncoding = sinon.stub().returns(this.proc.stderr) this.child_process.spawn = sinon.stub().returns(this.proc) this.CompileManager.clearProject( this.project_id, @@ -328,6 +329,7 @@ describe('CompileManager', function() { this.proc = new EventEmitter() this.proc.stdout = new EventEmitter() this.proc.stderr = new EventEmitter() + this.proc.stderr.setEncoding = sinon.stub().returns(this.proc.stderr) this.child_process.spawn = sinon.stub().returns(this.proc) this.CompileManager.clearProject( this.project_id, diff --git a/test/unit/js/OutputFileFinderTests.js b/test/unit/js/OutputFileFinderTests.js index e5f99046..ee591b40 100644 --- a/test/unit/js/OutputFileFinderTests.js +++ b/test/unit/js/OutputFileFinderTests.js @@ -70,6 +70,7 @@ describe('OutputFileFinder', function() { beforeEach(function() { this.proc = new EventEmitter() this.proc.stdout = new EventEmitter() + this.proc.stdout.setEncoding = sinon.stub().returns(this.proc.stdout) this.spawn.returns(this.proc) this.directory = '/base/dir' return this.OutputFileFinder._getAllFiles(this.directory, this.callback) From 3592ffda52e7f661d501ead3e50723c672d80049 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 7 May 2020 10:42:05 +0100 Subject: [PATCH 527/709] fix deprecated usage of Buffer constructor --- app/js/OutputFileOptimiser.js | 3 +-- app/js/SafeReader.js | 2 +- test/unit/js/OutputFileOptimiserTests.js | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/js/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js index c0b8cc14..1af20452 100644 --- a/app/js/OutputFileOptimiser.js +++ b/app/js/OutputFileOptimiser.js @@ -45,8 +45,7 @@ module.exports = OutputFileOptimiser = { checkIfPDFIsOptimised(file, callback) { const SIZE = 16 * 1024 // check the header of the pdf - const result = new Buffer(SIZE) - result.fill(0) // prevent leakage of uninitialised buffer + const result = Buffer.alloc(SIZE) // fills with zeroes by default return fs.open(file, 'r', function(err, fd) { if (err != null) { return callback(err) diff --git a/app/js/SafeReader.js b/app/js/SafeReader.js index d909e37e..90082675 100644 --- a/app/js/SafeReader.js +++ b/app/js/SafeReader.js @@ -43,7 +43,7 @@ module.exports = SafeReader = { } return callback(null, ...Array.from(result)) }) - const buff = new Buffer(size, 0) // fill with zeros + const buff = Buffer.alloc(size) // fills with zeroes by default return fs.read(fd, buff, 0, buff.length, 0, function( err, bytesRead, diff --git a/test/unit/js/OutputFileOptimiserTests.js b/test/unit/js/OutputFileOptimiserTests.js index 4546f084..b4983bf0 100644 --- a/test/unit/js/OutputFileOptimiserTests.js +++ b/test/unit/js/OutputFileOptimiserTests.js @@ -124,7 +124,7 @@ describe('OutputFileOptimiser', function() { this.fs.read = sinon .stub() .withArgs(this.fd) - .yields(null, 100, new Buffer('hello /Linearized 1')) + .yields(null, 100, Buffer.from('hello /Linearized 1')) this.fs.close = sinon .stub() .withArgs(this.fd) @@ -140,7 +140,7 @@ describe('OutputFileOptimiser', function() { this.fs.read = sinon .stub() .withArgs(this.fd) - .yields(null, 100, new Buffer('hello /Linearized 1')) + .yields(null, 100, Buffer.from('hello /Linearized 1')) return this.OutputFileOptimiser.checkIfPDFIsOptimised( this.src, this.callback @@ -169,7 +169,7 @@ describe('OutputFileOptimiser', function() { this.fs.read = sinon .stub() .withArgs(this.fd) - .yields(null, 100, new Buffer('hello not linearized 1')) + .yields(null, 100, Buffer.from('hello not linearized 1')) return this.OutputFileOptimiser.checkIfPDFIsOptimised( this.src, this.callback From 0bd99a3edcfe68967b26ac7e8694675e4e21fe12 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@gmail.com> Date: Thu, 14 May 2020 13:09:57 +0100 Subject: [PATCH 528/709] add pipeUrlToFileWithRetry function to retry file downloads 3 times --- app/js/UrlCache.js | 2 +- app/js/UrlFetcher.js | 9 +- test/acceptance/js/UrlCachingTests.js | 1 - test/unit/js/UrlCacheTests.js | 6 +- test/unit/js/UrlFetcherTests.js | 236 +++++++++++++++----------- 5 files changed, 149 insertions(+), 105 deletions(-) diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index 9b982e5e..df9c175a 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -95,7 +95,7 @@ module.exports = UrlCache = { } if (needsDownloading) { logger.log({ url, lastModified }, 'downloading URL') - return UrlFetcher.pipeUrlToFile( + return UrlFetcher.pipeUrlToFileWithRetry( url, UrlCache._cacheFilePathForUrl(project_id, url), error => { diff --git a/app/js/UrlFetcher.js b/app/js/UrlFetcher.js index 19c681ca..6c7d83af 100644 --- a/app/js/UrlFetcher.js +++ b/app/js/UrlFetcher.js @@ -12,16 +12,23 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let UrlFetcher const request = require('request').defaults({ jar: false }) const fs = require('fs') const logger = require('logger-sharelatex') const settings = require('settings-sharelatex') const URL = require('url') +const async = require('async') const oneMinute = 60 * 1000 module.exports = UrlFetcher = { + pipeUrlToFileWithRetry(url, filePath, callback) { + const doDownload = function(cb) { + UrlFetcher.pipeUrlToFile(url, filePath, cb) + } + async.retry(3, doDownload, callback) + }, + pipeUrlToFile(url, filePath, _callback) { if (_callback == null) { _callback = function(error) {} diff --git a/test/acceptance/js/UrlCachingTests.js b/test/acceptance/js/UrlCachingTests.js index 4d624978..b86541b6 100644 --- a/test/acceptance/js/UrlCachingTests.js +++ b/test/acceptance/js/UrlCachingTests.js @@ -11,7 +11,6 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const Client = require('./helpers/Client') -const request = require('request') require('chai').should() const sinon = require('sinon') const ClsiApp = require('./helpers/ClsiApp') diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index f056a6eb..f5c0f3e2 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -160,7 +160,7 @@ describe('UrlCache', function() { describe('_ensureUrlIsInCache', function() { beforeEach(function() { - this.UrlFetcher.pipeUrlToFile = sinon.stub().callsArg(2) + this.UrlFetcher.pipeUrlToFileWithRetry = sinon.stub().callsArg(2) return (this.UrlCache._updateOrCreateUrlDetails = sinon .stub() .callsArg(3)) @@ -190,7 +190,7 @@ describe('UrlCache', function() { }) it('should download the URL to the cache file', function() { - return this.UrlFetcher.pipeUrlToFile + return this.UrlFetcher.pipeUrlToFileWithRetry .calledWith( this.url, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url) @@ -232,7 +232,7 @@ describe('UrlCache', function() { }) it('should not download the URL to the cache file', function() { - return this.UrlFetcher.pipeUrlToFile.called.should.equal(false) + return this.UrlFetcher.pipeUrlToFileWithRetry.called.should.equal(false) }) return it('should return the callback with the cache file path', function() { diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index c435f450..24792097 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -33,72 +33,64 @@ describe('UrlFetcher', function() { } })) }) + describe('pipeUrlToFileWithRetry', function() { + this.beforeEach(function() { + this.UrlFetcher.pipeUrlToFile = sinon.stub() + }) - it('should turn off the cookie jar in request', function() { - return this.defaults.calledWith({ jar: false }).should.equal(true) - }) - - describe('rewrite url domain if filestoreDomainOveride is set', function() { - beforeEach(function() { - this.path = '/path/to/file/on/disk' - this.request.get = sinon - .stub() - .returns((this.urlStream = new EventEmitter())) - this.urlStream.pipe = sinon.stub() - this.urlStream.pause = sinon.stub() - this.urlStream.resume = sinon.stub() - this.fs.createWriteStream = sinon - .stub() - .returns((this.fileStream = new EventEmitter())) - return (this.fs.unlink = (file, callback) => callback()) + it('should call pipeUrlToFile', function(done) { + this.UrlFetcher.pipeUrlToFile.callsArgWith(2) + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { + expect(err).to.equal(undefined) + this.UrlFetcher.pipeUrlToFile.called.should.equal(true) + done() + }) }) - it('should use the normal domain when override not set', function(done) { - this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { - this.request.get.args[0][0].url.should.equal(this.url) - return done() + it('should call pipeUrlToFile multiple times on error', function(done) { + error = new Error("couldn't download file") + this.UrlFetcher.pipeUrlToFile.callsArgWith(2, error) + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { + expect(err).to.equal(error) + this.UrlFetcher.pipeUrlToFile.callCount.should.equal(3) + done() }) - this.res = { statusCode: 200 } - this.urlStream.emit('response', this.res) - this.urlStream.emit('end') - return this.fileStream.emit('finish') }) - return it('should use override domain when filestoreDomainOveride is set', function(done) { - this.settings.filestoreDomainOveride = '192.11.11.11' - this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { - this.request.get.args[0][0].url.should.equal( - '192.11.11.11/file/here?query=string' - ) - return done() + it('should call pipeUrlToFile twice if only 1 error', function(done) { + this.UrlFetcher.pipeUrlToFile.onCall(0).callsArgWith(2, 'error') + this.UrlFetcher.pipeUrlToFile.onCall(1).callsArgWith(2) + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { + expect(err).to.equal(undefined) + this.UrlFetcher.pipeUrlToFile.callCount.should.equal(2) + done() }) - this.res = { statusCode: 200 } - this.urlStream.emit('response', this.res) - this.urlStream.emit('end') - return this.fileStream.emit('finish') }) }) - return describe('pipeUrlToFile', function() { - beforeEach(function(done) { - this.path = '/path/to/file/on/disk' - this.request.get = sinon - .stub() - .returns((this.urlStream = new EventEmitter())) - this.urlStream.pipe = sinon.stub() - this.urlStream.pause = sinon.stub() - this.urlStream.resume = sinon.stub() - this.fs.createWriteStream = sinon - .stub() - .returns((this.fileStream = new EventEmitter())) - this.fs.unlink = (file, callback) => callback() - return done() + describe('pipeUrlToFile', function() { + it('should turn off the cookie jar in request', function() { + return this.defaults.calledWith({ jar: false }).should.equal(true) }) - describe('successfully', function() { - beforeEach(function(done) { + describe('rewrite url domain if filestoreDomainOveride is set', function() { + beforeEach(function() { + this.path = '/path/to/file/on/disk' + this.request.get = sinon + .stub() + .returns((this.urlStream = new EventEmitter())) + this.urlStream.pipe = sinon.stub() + this.urlStream.pause = sinon.stub() + this.urlStream.resume = sinon.stub() + this.fs.createWriteStream = sinon + .stub() + .returns((this.fileStream = new EventEmitter())) + return (this.fs.unlink = (file, callback) => callback()) + }) + + it('should use the normal domain when override not set', function(done) { this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { - this.callback() + this.request.get.args[0][0].url.should.equal(this.url) return done() }) this.res = { statusCode: 200 } @@ -107,67 +99,113 @@ describe('UrlFetcher', function() { return this.fileStream.emit('finish') }) - it('should request the URL', function() { - return this.request.get - .calledWith(sinon.match({ url: this.url })) - .should.equal(true) + return it('should use override domain when filestoreDomainOveride is set', function(done) { + this.settings.filestoreDomainOveride = '192.11.11.11' + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.request.get.args[0][0].url.should.equal( + '192.11.11.11/file/here?query=string' + ) + return done() + }) + this.res = { statusCode: 200 } + this.urlStream.emit('response', this.res) + this.urlStream.emit('end') + return this.fileStream.emit('finish') }) + }) - it('should open the file for writing', function() { - return this.fs.createWriteStream - .calledWith(this.path) - .should.equal(true) + return describe('pipeUrlToFile', function() { + beforeEach(function(done) { + this.path = '/path/to/file/on/disk' + this.request.get = sinon + .stub() + .returns((this.urlStream = new EventEmitter())) + this.urlStream.pipe = sinon.stub() + this.urlStream.pause = sinon.stub() + this.urlStream.resume = sinon.stub() + this.fs.createWriteStream = sinon + .stub() + .returns((this.fileStream = new EventEmitter())) + this.fs.unlink = (file, callback) => callback() + return done() }) - it('should pipe the URL to the file', function() { - return this.urlStream.pipe - .calledWith(this.fileStream) - .should.equal(true) - }) + describe('successfully', function() { + beforeEach(function(done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { + this.callback() + return done() + }) + this.res = { statusCode: 200 } + this.urlStream.emit('response', this.res) + this.urlStream.emit('end') + return this.fileStream.emit('finish') + }) - return it('should call the callback', function() { - return this.callback.called.should.equal(true) - }) - }) + it('should request the URL', function() { + return this.request.get + .calledWith(sinon.match({ url: this.url })) + .should.equal(true) + }) - describe('with non success status code', function() { - beforeEach(function(done) { - this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { - this.callback(err) - return done() + it('should open the file for writing', function() { + return this.fs.createWriteStream + .calledWith(this.path) + .should.equal(true) }) - this.res = { statusCode: 404 } - this.urlStream.emit('response', this.res) - return this.urlStream.emit('end') - }) - it('should call the callback with an error', function() { - this.callback.calledWith(sinon.match(Error)).should.equal(true) + it('should pipe the URL to the file', function() { + return this.urlStream.pipe + .calledWith(this.fileStream) + .should.equal(true) + }) - const message = this.callback.args[0][0].message - expect(message).to.include('URL returned non-success status code: 404') + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) }) - }) - return describe('with error', function() { - beforeEach(function(done) { - this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { - this.callback(err) - return done() + describe('with non success status code', function() { + beforeEach(function(done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { + this.callback(err) + return done() + }) + this.res = { statusCode: 404 } + this.urlStream.emit('response', this.res) + return this.urlStream.emit('end') }) - return this.urlStream.emit( - 'error', - (this.error = new Error('something went wrong')) - ) - }) - it('should call the callback with the error', function() { - return this.callback.calledWith(this.error).should.equal(true) + it('should call the callback with an error', function() { + this.callback.calledWith(sinon.match(Error)).should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include( + 'URL returned non-success status code: 404' + ) + }) }) - return it('should only call the callback once, even if end is called', function() { - this.urlStream.emit('end') - return this.callback.calledOnce.should.equal(true) + return describe('with error', function() { + beforeEach(function(done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { + this.callback(err) + return done() + }) + return this.urlStream.emit( + 'error', + (this.error = new Error('something went wrong')) + ) + }) + + it('should call the callback with the error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + + return it('should only call the callback once, even if end is called', function() { + this.urlStream.emit('end') + return this.callback.calledOnce.should.equal(true) + }) }) }) }) From 5ab45c1031d37414d77e42aff2db7459d63e2e52 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 15 May 2020 16:08:10 +0100 Subject: [PATCH 529/709] record latexmk output --- app/js/CompileManager.js | 5 +++++ app/js/ResourceWriter.js | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 3bf54bc7..500adfce 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -217,6 +217,11 @@ module.exports = CompileManager = { error = new Error('compilation') error.validate = 'fail' } + // make top-level output accesible to user, write in background for simplicity + if (output != null) { + fs.writeFile(Path.join(compileDir, "output.stdout"), output.stdout, () => { }) + fs.writeFile(Path.join(compileDir, "output.stderr"), output.stderr, () => { }) + } // compile was killed by user, was a validation, or a compile which failed validation if ( (error != null ? error.terminated : undefined) || diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index ed3a1e87..750be323 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -201,6 +201,10 @@ module.exports = ResourceWriter = { // knitr cache should_delete = false } + if (path.match(/^output.(stdout|stderr)$/)) { + // latexmk output + should_delete = true + } if (path.match(/^output-.*/)) { // Tikz cached figures (default case) should_delete = false From c004d299c1328eb1f166adb0357ba4c9cf835d69 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@gmail.com> Date: Mon, 11 May 2020 10:29:16 +0100 Subject: [PATCH 530/709] add refreshExpiryTimeout function on clsi all data lives inside of / dir dynamically reduce size of EXPIRY_TIMEOUT if disk starts to get full --- app.js | 8 +-- app/js/ProjectPersistenceManager.js | 22 +++++++ package-lock.json | 14 +++++ package.json | 1 + .../unit/js/ProjectPersistenceManagerTests.js | 62 ++++++++++++++++++- 5 files changed, 102 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 80b00ef5..6fd29b0a 100644 --- a/app.js +++ b/app.js @@ -359,10 +359,10 @@ if (!module.parent) { module.exports = app -setInterval( - () => ProjectPersistenceManager.clearExpiredProjects(), - (tenMinutes = 10 * 60 * 1000) -) +setInterval(() => { + ProjectPersistenceManager.refreshExpiryTimeout() + ProjectPersistenceManager.clearExpiredProjects() +}, (tenMinutes = 10 * 60 * 1000)) function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index 46eee747..2c89f13f 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -20,10 +20,32 @@ const async = require('async') const logger = require('logger-sharelatex') const oneDay = 24 * 60 * 60 * 1000 const Settings = require('settings-sharelatex') +const diskusage = require('diskusage') module.exports = ProjectPersistenceManager = { EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5, + refreshExpiryTimeout(callback) { + if (callback == null) { + callback = function(error) {} + } + diskusage.check('/', function(err, stats) { + if (err) { + logger.err({ err: err }, 'error getting disk usage') + return callback(err) + } + const lowDisk = stats.available / stats.total < 0.1 + const lowerExpiry = ProjectPersistenceManager.EXPIRY_TIMEOUT * 0.9 + if (lowDisk && Settings.project_cache_length_ms / 2 < lowerExpiry) { + logger.warn( + { stats: stats }, + 'disk running low on space, modifying EXPIRY_TIMEOUT' + ) + ProjectPersistenceManager.EXPIRY_TIMEOUT = lowerExpiry + } + callback() + }) + }, markProjectAsJustAccessed(project_id, callback) { if (callback == null) { callback = function(error) {} diff --git a/package-lock.json b/package-lock.json index c64e7d44..2d27ff12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1883,6 +1883,15 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" }, + "diskusage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.1.3.tgz", + "integrity": "sha512-EAyaxl8hy4Ph07kzlzGTfpbZMNAAAHXSZtNEMwdlnSd1noHzvA6HsgKt4fEMSvaEXQYLSphe5rPMxN4WOj0hcQ==", + "requires": { + "es6-promise": "^4.2.5", + "nan": "^2.14.0" + } + }, "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -6925,6 +6934,11 @@ "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" }, + "when": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", + "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 00440a8b..5c25fa12 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "async": "3.2.0", "body-parser": "^1.19.0", + "diskusage": "^1.1.3", "dockerode": "^3.1.0", "express": "^4.17.1", "fs-extra": "^8.1.0", diff --git a/test/unit/js/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js index 0d84fc24..1a12cfff 100644 --- a/test/unit/js/ProjectPersistenceManagerTests.js +++ b/test/unit/js/ProjectPersistenceManagerTests.js @@ -14,6 +14,7 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') require('chai').should() +const assert = require('chai').assert const modulePath = require('path').join( __dirname, '../../../app/js/ProjectPersistenceManager' @@ -26,7 +27,15 @@ describe('ProjectPersistenceManager', function() { requires: { './UrlCache': (this.UrlCache = {}), './CompileManager': (this.CompileManager = {}), - 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + diskusage: (this.diskusage = { check: sinon.stub() }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + warn: sinon.stub(), + err: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + project_cache_length_ms: 1000 + }), './db': (this.db = {}) } }) @@ -35,6 +44,57 @@ describe('ProjectPersistenceManager', function() { return (this.user_id = '1234') }) + describe('refreshExpiryTimeout', function() { + it('should leave expiry alone if plenty of disk', function(done) { + this.diskusage.check.callsArgWith(1, null, { + available: 40, + total: 100 + }) + + this.ProjectPersistenceManager.refreshExpiryTimeout(() => { + this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal( + this.settings.project_cache_length_ms + ) + done() + }) + }) + + it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function(done) { + this.diskusage.check.callsArgWith(1, null, { + available: 5, + total: 100 + }) + + this.ProjectPersistenceManager.refreshExpiryTimeout(() => { + this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900) + done() + }) + }) + + it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function(done) { + this.diskusage.check.callsArgWith(1, null, { + available: 5, + total: 100 + }) + this.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500 + this.ProjectPersistenceManager.refreshExpiryTimeout(() => { + this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500) + done() + }) + }) + + it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', function(done) { + this.diskusage.check.callsArgWith(1, 'Error', { + available: 5, + total: 100 + }) + this.ProjectPersistenceManager.refreshExpiryTimeout(() => { + this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000) + done() + }) + }) + }) + describe('clearExpiredProjects', function() { beforeEach(function() { this.project_ids = ['project-id-1', 'project-id-2'] From b8125e396a7458d48b53a5b9ad38ffb4ed7b2e6c Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Thu, 7 May 2020 18:57:35 +0200 Subject: [PATCH 531/709] [misc] simplify the smoke test and process shutdown --- app.js | 61 +++++++--------------- package-lock.json | 100 ------------------------------------ package.json | 1 - test/smoke/js/SmokeTests.js | 90 ++++++++++++++++---------------- 4 files changed, 64 insertions(+), 188 deletions(-) diff --git a/app.js b/app.js index 6fd29b0a..311fc31e 100644 --- a/app.js +++ b/app.js @@ -17,7 +17,7 @@ if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { logger.initializeErrorReporting(Settings.sentry.dsn) } -const smokeTest = require('smoke-test-sharelatex') +const smokeTest = require('./test/smoke/js/SmokeTests') const ContentTypeMapper = require('./app/js/ContentTypeMapper') const Errors = require('./app/js/Errors') @@ -192,64 +192,39 @@ app.get('/oops', function(req, res, next) { app.get('/status', (req, res, next) => res.send('CLSI is alive\n')) -const resCacher = { - contentType(setContentType) { - this.setContentType = setContentType - }, - send(code, body) { - this.code = code - this.body = body - }, - - // default the server to be down - code: 500, - body: {}, - setContentType: 'application/json' -} - -let shutdownTime +let PROCESS_IS_TOO_OLD = false if (Settings.processLifespanLimitMs) { Settings.processLifespanLimitMs += Settings.processLifespanLimitMs * (Math.random() / 10) - shutdownTime = Date.now() + Settings.processLifespanLimitMs - logger.info('Lifespan limited to ', shutdownTime) -} + logger.info( + 'Lifespan limited to ', + Date.now() + Settings.processLifespanLimitMs + ) -const checkIfProcessIsTooOld = function(cont) { - if (shutdownTime && shutdownTime < Date.now()) { + setTimeout(() => { logger.log('shutting down, process is too old') - resCacher.send = function() {} - resCacher.code = 500 - resCacher.body = { processToOld: true } - } else { - cont() - } + PROCESS_IS_TOO_OLD = true + }, Settings.processLifespanLimitMs) } if (Settings.smokeTest) { - const runSmokeTest = function() { - checkIfProcessIsTooOld(function() { - logger.log('running smoke tests') - smokeTest.run( - require.resolve(__dirname + '/test/smoke/js/SmokeTests.js') - )({}, resCacher) - return setTimeout(runSmokeTest, 30 * 1000) + function runSmokeTest() { + if (PROCESS_IS_TOO_OLD) return + logger.log('running smoke tests') + smokeTest.triggerRun(err => { + logger.error({ err }, 'smoke tests failed') + setTimeout(runSmokeTest, 30 * 1000) }) } runSmokeTest() } app.get('/health_check', function(req, res) { - res.contentType(resCacher.setContentType) - return res.status(resCacher.code).send(resCacher.body) + if (PROCESS_IS_TOO_OLD) return res.status(500).json({ processToOld: true }) + smokeTest.sendLastResult(res) }) -app.get('/smoke_test_force', (req, res) => - smokeTest.run(require.resolve(__dirname + '/test/smoke/js/SmokeTests.js'))( - req, - res - ) -) +app.get('/smoke_test_force', (req, res) => smokeTest.sendNewResult(res)) app.use(function(error, req, res, next) { if (error instanceof Errors.NotFoundError) { diff --git a/package-lock.json b/package-lock.json index 2d27ff12..7f896457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1669,11 +1669,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.0.0", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=" - }, "common-tags": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", @@ -1878,11 +1873,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, - "diff": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=" - }, "diskusage": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.1.3.tgz", @@ -3066,11 +3056,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, - "growl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=" - }, "gtoken": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.0.tgz", @@ -3576,27 +3561,6 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" - } - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6062,11 +6026,6 @@ "object-inspect": "^1.7.0" } }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -6129,65 +6088,6 @@ } } }, - "smoke-test-sharelatex": { - "version": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#bc3e93d18ccee219c0d99e8b02c984ccdd842e1c", - "from": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", - "requires": { - "mocha": "~1.17.0" - }, - "dependencies": { - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "requires": { - "graceful-fs": "~2.0.0", - "inherits": "2", - "minimatch": "~0.2.11" - } - }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "requires": { - "lru-cache": "2", - "sigmund": "~1.0.0" - } - }, - "mocha": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", - "integrity": "sha1-f3Zx1oUm0HS3uuZgyQmfh+DqHMs=", - "requires": { - "commander": "2.0.0", - "debug": "*", - "diff": "1.0.7", - "glob": "3.2.3", - "growl": "1.7.x", - "jade": "0.26.3", - "mkdirp": "0.3.5" - }, - "dependencies": { - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" - } - } - } - } - }, "snakecase-keys": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.1.0.tgz", diff --git a/package.json b/package.json index 5c25fa12..e57bfa55 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "request": "^2.88.2", "sequelize": "^5.21.5", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", - "smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v0.2.0", "sqlite3": "^4.1.1", "underscore": "^1.9.2", "v8-profiler-node8": "^6.1.1", diff --git a/test/smoke/js/SmokeTests.js b/test/smoke/js/SmokeTests.js index 851ea850..fc52121e 100644 --- a/test/smoke/js/SmokeTests.js +++ b/test/smoke/js/SmokeTests.js @@ -1,20 +1,3 @@ -/* eslint-disable - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const chai = require('chai') -if (Object.prototype.should == null) { - chai.should() -} -const { expect } = chai const request = require('request') const Settings = require('settings-sharelatex') @@ -23,9 +6,35 @@ const buildUrl = path => const url = buildUrl(`project/smoketest-${process.pid}/compile`) -describe('Running a compile', function() { - before(function(done) { - return request.post( +module.exports = { + sendNewResult(res) { + this._run(error => this._sendResponse(res, error)) + }, + sendLastResult(res) { + this._sendResponse(res, this._lastError) + }, + triggerRun(cb) { + this._run(error => { + this._lastError = error + cb(error) + }) + }, + + _lastError: new Error('SmokeTestsPending'), + _sendResponse(res, error) { + let code, body + if (error) { + code = 500 + body = error.message + } else { + code = 200 + body = 'OK' + } + res.contentType('text/plain') + res.status(code).send(body) + }, + _run(done) { + request.post( { url, json: { @@ -50,7 +59,7 @@ describe('Running a compile', function() { \\pgfmathsetmacro{\\dy}{rand*0.1}% A random variance in the y coordinate, % gives a hight fill to the lipid \\pgfmathsetmacro{\\rot}{rand*0.1}% A random variance in the - % molecule orientation + % molecule orientation \\shade[ball color=red] ({\\i+\\dx+\\rot},{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)}) circle(0.45); \\shade[ball color=gray] (\\i+\\dx,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-0.9}) circle(0.45); \\shade[ball color=gray] (\\i+\\dx-\\rot,{0.5*\\j+\\dy+0.4*sin(\\i*\\nuPi*10)-1.8}) circle(0.45); @@ -72,29 +81,22 @@ describe('Running a compile', function() { } }, (error, response, body) => { - this.error = error - this.response = response - this.body = body - return done() - } - ) - }) + if (error) return done(error) + if (!body || !body.compile || !body.compile.outputFiles) { + return done(new Error('response payload incomplete')) + } - it('should return the pdf', function() { - for (const file of Array.from(this.body.compile.outputFiles)) { - if (file.type === 'pdf') { - return - } - } - throw new Error('no pdf returned') - }) + let pdfFound = false + let logFound = false + for (const file of body.compile.outputFiles) { + if (file.type === 'pdf') pdfFound = true + if (file.type === 'log') logFound = true + } - return it('should return the log', function() { - for (const file of Array.from(this.body.compile.outputFiles)) { - if (file.type === 'log') { - return + if (!pdfFound) return done(new Error('no pdf returned')) + if (!logFound) return done(new Error('no log returned')) + done() } - } - throw new Error('no log returned') - }) -}) + ) + } +} From 9807b515190368582de4182fba5e54a57ea73e73 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 11 May 2020 13:08:13 +0200 Subject: [PATCH 532/709] [misc] apply review feedback --- app.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 311fc31e..add5f217 100644 --- a/app.js +++ b/app.js @@ -192,7 +192,7 @@ app.get('/oops', function(req, res, next) { app.get('/status', (req, res, next) => res.send('CLSI is alive\n')) -let PROCESS_IS_TOO_OLD = false +Settings.processTooOld = false if (Settings.processLifespanLimitMs) { Settings.processLifespanLimitMs += Settings.processLifespanLimitMs * (Math.random() / 10) @@ -203,16 +203,16 @@ if (Settings.processLifespanLimitMs) { setTimeout(() => { logger.log('shutting down, process is too old') - PROCESS_IS_TOO_OLD = true + Settings.processTooOld = true }, Settings.processLifespanLimitMs) } if (Settings.smokeTest) { function runSmokeTest() { - if (PROCESS_IS_TOO_OLD) return + if (Settings.processTooOld) return logger.log('running smoke tests') smokeTest.triggerRun(err => { - logger.error({ err }, 'smoke tests failed') + if (err) logger.error({ err }, 'smoke tests failed') setTimeout(runSmokeTest, 30 * 1000) }) } @@ -220,7 +220,9 @@ if (Settings.smokeTest) { } app.get('/health_check', function(req, res) { - if (PROCESS_IS_TOO_OLD) return res.status(500).json({ processToOld: true }) + if (Settings.processTooOld) { + return res.status(500).json({ processTooOld: true }) + } smokeTest.sendLastResult(res) }) From 54896fb15784fea08eb997d4d05e16d349b36dbb Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 20 May 2020 11:45:29 +0100 Subject: [PATCH 533/709] clean up the stdout/stderr recording --- app/js/CompileManager.js | 5 ----- app/js/LatexRunner.js | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 500adfce..3bf54bc7 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -217,11 +217,6 @@ module.exports = CompileManager = { error = new Error('compilation') error.validate = 'fail' } - // make top-level output accesible to user, write in background for simplicity - if (output != null) { - fs.writeFile(Path.join(compileDir, "output.stdout"), output.stdout, () => { }) - fs.writeFile(Path.join(compileDir, "output.stderr"), output.stderr, () => { }) - } // compile was killed by user, was a validation, or a compile which failed validation if ( (error != null ? error.terminated : undefined) || diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index 972f1fe7..fe737c76 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -19,6 +19,7 @@ const Settings = require('settings-sharelatex') const logger = require('logger-sharelatex') const Metrics = require('./Metrics') const CommandRunner = require('./CommandRunner') +const fs = require('fs') const ProcessTable = {} // table of currently running jobs (pids or docker container names) @@ -127,9 +128,36 @@ module.exports = LatexRunner = { : undefined, x5 => x5[1] ) || 0 - return callback(error, output, stats, timings) + // record output files + LatexRunner.writeLogOutput(project_id, directory, output, () => { + return callback(error, output, stats, timings) + }) + })) + }, + + writeLogOutput(project_id, directory, output, callback) { + if (!output) { + return callback() + } + // internal method for writing non-empty log files + function _writeFile(file, content, cb) { + if (content && content.length > 0) { + fs.writeFile(file, content, (err) => { + if (err) { + logger.error({ project_id, file }, "error writing log file") // don't fail on error + } + cb() + }) + } else { + cb() } - )) + } + // write stdout and stderr, ignoring errors + _writeFile(Path.join(directory, "output.stdout"), output.stdout, () => { + _writeFile(Path.join(directory, "output.stderr"), output.stderr, () => { + callback() + }) + }) }, killLatex(project_id, callback) { From e3c278e7085a466c545d5f614e0b2e5df9bdfa56 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 20 May 2020 11:52:53 +0100 Subject: [PATCH 534/709] add unit tests --- test/unit/js/LatexRunnerTests.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index b468b831..15902e9b 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -37,7 +37,10 @@ describe('LatexRunner', function() { done() {} }) }, - './CommandRunner': (this.CommandRunner = {}) + './CommandRunner': (this.CommandRunner = {}), + 'fs': (this.fs = { + writeFile: sinon.stub().callsArg(2) + }) } }) @@ -83,6 +86,21 @@ describe('LatexRunner', function() { ) .should.equal(true) }) + + it('should record the stdout and stderr', function () { + this.fs.writeFile + .calledWith( + this.directory + '/' + 'output.stdout', + "this is stdout" + ) + .should.equal(true) + this.fs.writeFile + .calledWith( + this.directory + '/' + 'output.stderr', + "this is stderr" + ) + .should.equal(true) + }) }) describe('with an .Rtex main file', function() { From 1bcb370ca1a55a60730efa6e3b97d46fb8081893 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 20 May 2020 14:12:08 +0100 Subject: [PATCH 535/709] clean up log file deletion and add unit test --- app/js/ResourceWriter.js | 8 +++----- test/unit/js/ResourceWriterTests.js | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index 750be323..97e971e1 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -201,10 +201,6 @@ module.exports = ResourceWriter = { // knitr cache should_delete = false } - if (path.match(/^output.(stdout|stderr)$/)) { - // latexmk output - should_delete = true - } if (path.match(/^output-.*/)) { // Tikz cached figures (default case) should_delete = false @@ -235,7 +231,9 @@ module.exports = ResourceWriter = { path === 'output.pdf' || path === 'output.dvi' || path === 'output.log' || - path === 'output.xdv' + path === 'output.xdv' || + path === 'output.stdout' || + path === 'output.stderr' ) { should_delete = true } diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index a632c1bd..1080765b 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -230,6 +230,12 @@ describe('ResourceWriter', function() { { path: '_markdown_main/30893013dec5d869a415610079774c2f.md.tex', type: 'tex' + }, + { + path: 'output.stdout' + }, + { + path: 'output.stderr' } ] this.resources = 'mock-resources' @@ -256,7 +262,19 @@ describe('ResourceWriter', function() { .should.equal(true) }) - it('should delete the extra files', function() { + it('should delete the stdout log file', function () { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'output.stdout')) + .should.equal(true) + }) + + it('should delete the stderr log file', function () { + return this.ResourceWriter._deleteFileIfNotDirectory + .calledWith(path.join(this.basePath, 'output.stderr')) + .should.equal(true) + }) + + it('should delete the extra files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'extra/file.tex')) .should.equal(true) From f8cb5e36af09b41e43508735be6f2398bd7911bd Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Wed, 27 Nov 2019 21:40:48 +0100 Subject: [PATCH 536/709] [DockerRunner] destroyOldContainers: normalize the container name The docker api returns each name with a `/` prefix. In order to not interfere with pending compiles, the deletion process has to acquire an internal lock on the container. The LockManager uses the plain container name without the slash: `project-xxx`. Signed-off-by: Jakob Ackermann <das7pad@outlook.com> --- app/js/DockerRunner.js | 3 +++ test/unit/js/DockerRunnerTests.js | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index fd7fc317..3f90c811 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -632,6 +632,9 @@ module.exports = DockerRunner = { ttl ) { if (name.slice(0, 9) === '/project-' && ttl <= 0) { + // strip the / prefix + // the LockManager uses the plain container name + name = name.slice(1) return jobs.push(cb => DockerRunner.destroyContainer(name, id, false, () => cb()) ) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 83992833..d41ee3f7 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -630,19 +630,19 @@ describe('DockerRunner', function() { it('should destroy old containers', function() { this.DockerRunner.destroyContainer.callCount.should.equal(1) return this.DockerRunner.destroyContainer - .calledWith('/project-old-container-name', 'old-container-id') + .calledWith('project-old-container-name', 'old-container-id') .should.equal(true) }) it('should not destroy new containers', function() { return this.DockerRunner.destroyContainer - .calledWith('/project-new-container-name', 'new-container-id') + .calledWith('project-new-container-name', 'new-container-id') .should.equal(false) }) it('should not destroy non-project containers', function() { return this.DockerRunner.destroyContainer - .calledWith('/totally-not-a-project-container', 'some-random-id') + .calledWith('totally-not-a-project-container', 'some-random-id') .should.equal(false) }) From 17c14b119261da16e5b6b23e0bd19add5c08071c Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 2 Jun 2020 09:18:38 +0100 Subject: [PATCH 537/709] fix formatting with make format_fix --- app/js/DockerRunner.js | 2 +- app/js/LatexRunner.js | 11 ++++++----- test/unit/js/LatexRunnerTests.js | 14 ++++---------- test/unit/js/ResourceWriterTests.js | 6 +++--- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index e7cb2a73..c50087d0 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -412,7 +412,7 @@ module.exports = DockerRunner = { }) } ) - var inspectContainer = (isRetry) => + var inspectContainer = isRetry => container.inspect(function(error, stats) { if ((error != null ? error.statusCode : undefined) === 404) { return createAndStartContainer() diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index fe737c76..c3edb29e 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -132,7 +132,8 @@ module.exports = LatexRunner = { LatexRunner.writeLogOutput(project_id, directory, output, () => { return callback(error, output, stats, timings) }) - })) + } + )) }, writeLogOutput(project_id, directory, output, callback) { @@ -142,9 +143,9 @@ module.exports = LatexRunner = { // internal method for writing non-empty log files function _writeFile(file, content, cb) { if (content && content.length > 0) { - fs.writeFile(file, content, (err) => { + fs.writeFile(file, content, err => { if (err) { - logger.error({ project_id, file }, "error writing log file") // don't fail on error + logger.error({ project_id, file }, 'error writing log file') // don't fail on error } cb() }) @@ -153,8 +154,8 @@ module.exports = LatexRunner = { } } // write stdout and stderr, ignoring errors - _writeFile(Path.join(directory, "output.stdout"), output.stdout, () => { - _writeFile(Path.join(directory, "output.stderr"), output.stderr, () => { + _writeFile(Path.join(directory, 'output.stdout'), output.stdout, () => { + _writeFile(Path.join(directory, 'output.stderr'), output.stderr, () => { callback() }) }) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index 15902e9b..42e816d2 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -38,7 +38,7 @@ describe('LatexRunner', function() { }) }, './CommandRunner': (this.CommandRunner = {}), - 'fs': (this.fs = { + fs: (this.fs = { writeFile: sinon.stub().callsArg(2) }) } @@ -87,18 +87,12 @@ describe('LatexRunner', function() { .should.equal(true) }) - it('should record the stdout and stderr', function () { + it('should record the stdout and stderr', function() { this.fs.writeFile - .calledWith( - this.directory + '/' + 'output.stdout', - "this is stdout" - ) + .calledWith(this.directory + '/' + 'output.stdout', 'this is stdout') .should.equal(true) this.fs.writeFile - .calledWith( - this.directory + '/' + 'output.stderr', - "this is stderr" - ) + .calledWith(this.directory + '/' + 'output.stderr', 'this is stderr') .should.equal(true) }) }) diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 1080765b..36a95136 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -262,19 +262,19 @@ describe('ResourceWriter', function() { .should.equal(true) }) - it('should delete the stdout log file', function () { + it('should delete the stdout log file', function() { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'output.stdout')) .should.equal(true) }) - it('should delete the stderr log file', function () { + it('should delete the stderr log file', function() { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'output.stderr')) .should.equal(true) }) - it('should delete the extra files', function () { + it('should delete the extra files', function() { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'extra/file.tex')) .should.equal(true) From 440ec5553e233c3cd951f1b2db73225b960c2fb2 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 2 Jun 2020 09:19:39 +0100 Subject: [PATCH 538/709] fix unreachable code lint error --- test/unit/js/LatexRunnerTests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index 42e816d2..d9aa398d 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -74,7 +74,7 @@ describe('LatexRunner', function() { ) }) - return it('should run the latex command', function() { + it('should run the latex command', function() { return this.CommandRunner.run .calledWith( this.project_id, From 2211ebcefb897e11f4c78d5492554c9a242241b1 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 2 Jun 2020 09:51:34 +0100 Subject: [PATCH 539/709] fix eslint errors --- app.js | 60 ++++++++++++++++----------------- app/js/UrlFetcher.js | 1 + config/settings.defaults.js | 20 +++++------ test/unit/js/UrlFetcherTests.js | 2 +- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/app.js b/app.js index add5f217..319799a7 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,7 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let tenMinutes +const tenMinutes = 10 * 60 * 1000 const Metrics = require('metrics-sharelatex') Metrics.initialize('clsi') @@ -49,31 +49,29 @@ app.use(function(req, res, next) { return next() }) -app.param('project_id', function(req, res, next, project_id) { - if (project_id != null ? project_id.match(/^[a-zA-Z0-9_-]+$/) : undefined) { +app.param('project_id', function(req, res, next, projectId) { + if (projectId != null ? projectId.match(/^[a-zA-Z0-9_-]+$/) : undefined) { return next() } else { return next(new Error('invalid project id')) } }) -app.param('user_id', function(req, res, next, user_id) { - if (user_id != null ? user_id.match(/^[0-9a-f]{24}$/) : undefined) { +app.param('user_id', function(req, res, next, userId) { + if (userId != null ? userId.match(/^[0-9a-f]{24}$/) : undefined) { return next() } else { return next(new Error('invalid user id')) } }) -app.param('build_id', function(req, res, next, build_id) { +app.param('build_id', function(req, res, next, buildId) { if ( - build_id != null - ? build_id.match(OutputCacheManager.BUILD_REGEX) - : undefined + buildId != null ? buildId.match(OutputCacheManager.BUILD_REGEX) : undefined ) { return next() } else { - return next(new Error(`invalid build id ${build_id}`)) + return next(new Error(`invalid build id ${buildId}`)) } }) @@ -207,15 +205,15 @@ if (Settings.processLifespanLimitMs) { }, Settings.processLifespanLimitMs) } +function runSmokeTest() { + if (Settings.processTooOld) return + logger.log('running smoke tests') + smokeTest.triggerRun(err => { + if (err) logger.error({ err }, 'smoke tests failed') + setTimeout(runSmokeTest, 30 * 1000) + }) +} if (Settings.smokeTest) { - function runSmokeTest() { - if (Settings.processTooOld) return - logger.log('running smoke tests') - smokeTest.triggerRun(err => { - if (err) logger.error({ err }, 'smoke tests failed') - setTimeout(runSmokeTest, 30 * 1000) - }) - } runSmokeTest() } @@ -308,29 +306,31 @@ const host = x1 => x1.host ) || 'localhost' -const load_tcp_port = Settings.internal.load_balancer_agent.load_port -const load_http_port = Settings.internal.load_balancer_agent.local_port +const loadTcpPort = Settings.internal.load_balancer_agent.load_port +const loadHttpPort = Settings.internal.load_balancer_agent.local_port if (!module.parent) { // Called directly - app.listen(port, host, error => - logger.info(`CLSI starting up, listening on ${host}:${port}`) - ) + app.listen(port, host, error => { + if (error) { + logger.fatal({ error }, `Error starting CLSI on ${host}:${port}`) + } else { + logger.info(`CLSI starting up, listening on ${host}:${port}`) + } + }) - loadTcpServer.listen(load_tcp_port, host, function(error) { + loadTcpServer.listen(loadTcpPort, host, function(error) { if (error != null) { throw error } - return logger.info(`Load tcp agent listening on load port ${load_tcp_port}`) + return logger.info(`Load tcp agent listening on load port ${loadTcpPort}`) }) - loadHttpServer.listen(load_http_port, host, function(error) { + loadHttpServer.listen(loadHttpPort, host, function(error) { if (error != null) { throw error } - return logger.info( - `Load http agent listening on load port ${load_http_port}` - ) + return logger.info(`Load http agent listening on load port ${loadHttpPort}`) }) } @@ -339,7 +339,7 @@ module.exports = app setInterval(() => { ProjectPersistenceManager.refreshExpiryTimeout() ProjectPersistenceManager.clearExpiredProjects() -}, (tenMinutes = 10 * 60 * 1000)) +}, tenMinutes) function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null diff --git a/app/js/UrlFetcher.js b/app/js/UrlFetcher.js index 6c7d83af..f9f993f3 100644 --- a/app/js/UrlFetcher.js +++ b/app/js/UrlFetcher.js @@ -12,6 +12,7 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ +let UrlFetcher const request = require('request').defaults({ jar: false }) const fs = require('fs') const logger = require('logger-sharelatex') diff --git a/config/settings.defaults.js b/config/settings.defaults.js index ba63e243..51cb6248 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -9,7 +9,7 @@ module.exports = { username: 'clsi', dialect: 'sqlite', storage: - process.env.SQLITE_PATH || Path.resolve(__dirname + '/../db/db.sqlite'), + process.env.SQLITE_PATH || Path.resolve(__dirname, '../db/db.sqlite'), pool: { max: 1, min: 1 @@ -26,10 +26,10 @@ module.exports = { parseInt(process.env.PROCESS_LIFE_SPAN_LIMIT_MS) || 60 * 60 * 24 * 1000 * 2, path: { - compilesDir: Path.resolve(__dirname + '/../compiles'), - clsiCacheDir: Path.resolve(__dirname + '/../cache'), - synctexBaseDir(project_id) { - return Path.join(this.compilesDir, project_id) + compilesDir: Path.resolve(__dirname, '../compiles'), + clsiCacheDir: Path.resolve(__dirname, '../cache'), + synctexBaseDir(projectId) { + return Path.join(this.compilesDir, projectId) } }, @@ -63,7 +63,7 @@ module.exports = { } if (process.env.DOCKER_RUNNER) { - let seccomp_profile_path + let seccompProfilePath module.exports.clsi = { dockerRunner: process.env.DOCKER_RUNNER === 'true', docker: { @@ -81,16 +81,14 @@ if (process.env.DOCKER_RUNNER) { } try { - seccomp_profile_path = Path.resolve( - __dirname + '/../seccomp/clsi-profile.json' - ) + seccompProfilePath = Path.resolve(__dirname, '../seccomp/clsi-profile.json') module.exports.clsi.docker.seccomp_profile = JSON.stringify( - JSON.parse(require('fs').readFileSync(seccomp_profile_path)) + JSON.parse(require('fs').readFileSync(seccompProfilePath)) ) } catch (error) { console.log( error, - `could not load seccom profile from ${seccomp_profile_path}` + `could not load seccom profile from ${seccompProfilePath}` ) } diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index 24792097..57bee3a7 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -48,7 +48,7 @@ describe('UrlFetcher', function() { }) it('should call pipeUrlToFile multiple times on error', function(done) { - error = new Error("couldn't download file") + const error = new Error("couldn't download file") this.UrlFetcher.pipeUrlToFile.callsArgWith(2, error) this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { expect(err).to.equal(error) From bf2430f1fc17254285556f3d9f1e81a6b2cacc4b Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 2 Jun 2020 11:12:57 +0100 Subject: [PATCH 540/709] fix broken unit test --- test/unit/js/LatexRunnerTests.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index d9aa398d..b112b170 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -55,7 +55,10 @@ describe('LatexRunner', function() { return describe('runLatex', function() { beforeEach(function() { - return (this.CommandRunner.run = sinon.stub().callsArg(6)) + return (this.CommandRunner.run = sinon.stub().callsArgWith(6, null, { + stdout: 'this is stdout', + stderr: 'this is stderr' + })) }) describe('normally', function() { From d88136c569f4fc880e51df4464c79bf8e4aac788 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 3 Jun 2020 10:22:31 +0100 Subject: [PATCH 541/709] update to node 10.21.0 --- .nvmrc | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.nvmrc b/.nvmrc index 5b7269c0..b61c07ff 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.19.0 +10.21.0 diff --git a/Dockerfile b/Dockerfile index 40615ad8..9b378831 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -FROM node:10.19.0 as base +FROM node:10.21.0 as base WORKDIR /app COPY install_deps.sh /app From 59310cbb0993b551ddc7bd32d4124a25a61187e8 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 3 Jun 2020 11:11:51 +0100 Subject: [PATCH 542/709] update buildscript.txt to node 10.21.0 --- buildscript.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildscript.txt b/buildscript.txt index 81d65464..78ef1d04 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -6,6 +6,6 @@ clsi --env-add= --env-pass-through=TEXLIVE_IMAGE --language=es ---node-version=10.19.0 +--node-version=10.21.0 --public-repo=True --script-version=2.1.0 From 3e3e4503ebcc9af4de88d63e7472d2869dcb8256 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 4 Jun 2020 11:47:22 +0100 Subject: [PATCH 543/709] add setting TEXLIVE_OPENOUT_ANY --- app/js/CompileManager.js | 4 ++++ config/settings.defaults.js | 1 + 2 files changed, 5 insertions(+) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index dd62f435..614c49aa 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -142,6 +142,10 @@ module.exports = CompileManager = { ) // set up environment variables for chktex const env = {} + if (Settings.texliveOpenoutAny && Settings.texliveOpenoutAny !== '') { + // override default texlive openout_any environment variable + env.openout_any = Settings.texliveOpenoutAny + } // only run chktex on LaTeX files (not knitr .Rtex files or any others) const isLaTeXFile = request.rootResourcePath != null diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 51cb6248..3328afa9 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -57,6 +57,7 @@ module.exports = { parallelSqlQueryLimit: process.env.FILESTORE_PARALLEL_SQL_QUERY_LIMIT || 1, filestoreDomainOveride: process.env.FILESTORE_DOMAIN_OVERRIDE, texliveImageNameOveride: process.env.TEX_LIVE_IMAGE_NAME_OVERRIDE, + texliveOpenoutAny: process.env.TEXLIVE_OPENOUT_ANY, sentry: { dsn: process.env.SENTRY_DSN } From 385cdd6f0ca342b87ae484831cad03377289622f Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 9 Jun 2020 11:22:28 +0100 Subject: [PATCH 544/709] add missing setting for optimiseInDocker --- config/settings.defaults.js | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 51cb6248..d051cea8 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -76,6 +76,7 @@ if (process.env.DOCKER_RUNNER) { socketPath: '/var/run/docker.sock', user: process.env.TEXLIVE_IMAGE_USER || 'tex' }, + optimiseInDocker: true, expireProjectAfterIdleMs: 24 * 60 * 60 * 1000, checkProjectsIntervalMs: 10 * 60 * 1000 } From eb603f9f31cec434e81a3134274d98ff35795cff Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 10 Jun 2020 11:42:07 +0100 Subject: [PATCH 545/709] error on missing profile --- config/settings.defaults.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 3328afa9..2f74da0b 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -87,10 +87,11 @@ if (process.env.DOCKER_RUNNER) { JSON.parse(require('fs').readFileSync(seccompProfilePath)) ) } catch (error) { - console.log( + console.error( error, - `could not load seccom profile from ${seccompProfilePath}` + `could not load seccomp profile from ${seccompProfilePath}` ) + process.exit(1) } module.exports.path.synctexBaseDir = () => '/compile' From f077c337ec30b7a51eef09a9282ae9c547573414 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 11 Jun 2020 10:54:26 +0100 Subject: [PATCH 546/709] send 503 unavailable response on EPIPE --- app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.js b/app.js index 319799a7..b22e0a01 100644 --- a/app.js +++ b/app.js @@ -230,6 +230,9 @@ app.use(function(error, req, res, next) { if (error instanceof Errors.NotFoundError) { logger.warn({ err: error, url: req.url }, 'not found error') return res.sendStatus(404) + } else if (error.code === 'EPIPE') { + // inspect container returns EPIPE when shutting down + return res.sendStatus(503) // send 503 Unavailable response } else { logger.error({ err: error, url: req.url }, 'server error') return res.sendStatus((error != null ? error.statusCode : undefined) || 500) From 7ceadc859945cff40c18e711ec9d52c1adb99958 Mon Sep 17 00:00:00 2001 From: Tim Alby <timothee.alby@gmail.com> Date: Thu, 11 Jun 2020 12:50:43 +0200 Subject: [PATCH 547/709] partially revert "[DockerRunner] fix metric incrementing and error logging" This reverts commits: - 2b2fcca39ce8dee0fdc0c342aa0d6c822592bcec - 9e82ab0890c5cc8c7fb95362c3f7edbcaad0cf29 - e3da458b376871c3ce72d6984d14bf1ee668b04b --- app/js/DockerRunner.js | 36 +++++++------------ test/unit/js/DockerRunnerTests.js | 58 ------------------------------- 2 files changed, 13 insertions(+), 81 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index c50087d0..3f90c811 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -26,7 +26,6 @@ const LockManager = require('./DockerLockManager') const fs = require('fs') const Path = require('path') const _ = require('underscore') -const metrics = require('metrics-sharelatex') logger.info('using docker runner') @@ -412,28 +411,19 @@ module.exports = DockerRunner = { }) } ) - var inspectContainer = isRetry => - container.inspect(function(error, stats) { - if ((error != null ? error.statusCode : undefined) === 404) { - return createAndStartContainer() - } else if (error != null) { - if (error.message.match(/EPIPE/)) { - if (!isRetry) { - metrics.inc('container-inspect-epipe-retry') - return inspectContainer(true) - } - metrics.inc('container-inspect-epipe-error') - } - logger.err( - { container_name: name, error }, - 'unable to inspect container to start' - ) - return callback(error) - } else { - return startExistingContainer() - } - }) - inspectContainer(false) + return container.inspect(function(error, stats) { + if ((error != null ? error.statusCode : undefined) === 404) { + return createAndStartContainer() + } else if (error != null) { + logger.err( + { container_name: name, error }, + 'unable to inspect container to start' + ) + return callback(error) + } else { + return startExistingContainer() + } + }) }, attachToContainer(containerId, attachStreamHandler, attachStartCallback) { diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 1e44daff..aa45e88d 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -36,7 +36,6 @@ describe('DockerRunner', function() { 'logger-sharelatex': (this.logger = { log: sinon.stub(), error: sinon.stub(), - err: sinon.stub(), info: sinon.stub(), warn: sinon.stub() }), @@ -388,63 +387,6 @@ describe('DockerRunner', function() { }) }) - describe('when inspect always fails with EPIPE error', function() { - beforeEach(function() { - this.error = new Error('write EPIPE') - this.container.inspect = sinon.stub().yields(this.error) - this.container.start = sinon.stub().yields() - - this.DockerRunner.startContainer( - this.options, - this.volumes, - () => {}, - this.callback - ) - }) - - it('should retry once', function() { - sinon.assert.callOrder( - this.container.inspect, - this.container.inspect, - this.callback - ) - }) - - it('should call back with error', function() { - sinon.assert.calledWith(this.callback, this.error) - }) - }) - - describe('when inspect fails once with EPIPE error', function() { - beforeEach(function() { - this.container.inspect = sinon.stub() - this.container.inspect.onFirstCall().yields(new Error('write EPIPE')) - this.container.inspect.onSecondCall().yields() - this.container.start = sinon.stub().yields() - - this.DockerRunner.startContainer( - this.options, - this.volumes, - () => {}, - this.callback - ) - }) - - it('should retry once and start container', function() { - sinon.assert.callOrder( - this.container.inspect, - this.container.inspect, - this.DockerRunner.attachToContainer, - this.container.start, - this.callback - ) - }) - - it('should call back without error', function() { - sinon.assert.calledWith(this.callback, null) - }) - }) - describe('when the container does not exist', function() { beforeEach(function() { const exists = false From 33d6462875fe17aebb57d808736a2c1c58c329f0 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 12 Jun 2020 15:15:27 +0100 Subject: [PATCH 548/709] check output file exists before running synctex --- app/js/CompileManager.js | 119 ++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 70 deletions(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 614c49aa..4b0cff63 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -430,30 +430,18 @@ module.exports = CompileManager = { const compileDir = getCompileDir(project_id, user_id) const synctex_path = `${base_dir}/output.pdf` const command = ['code', synctex_path, file_path, line, column] - return fse.ensureDir(compileDir, function(error) { + CompileManager._runSynctex(project_id, user_id, command, function( + error, + stdout + ) { if (error != null) { - logger.err( - { error, project_id, user_id, file_name }, - 'error ensuring dir for sync from code' - ) return callback(error) } - return CompileManager._runSynctex(project_id, user_id, command, function( - error, - stdout - ) { - if (error != null) { - return callback(error) - } - logger.log( - { project_id, user_id, file_name, line, column, command, stdout }, - 'synctex code output' - ) - return callback( - null, - CompileManager._parseSynctexFromCodeOutput(stdout) - ) - }) + logger.log( + { project_id, user_id, file_name, line, column, command, stdout }, + 'synctex code output' + ) + return callback(null, CompileManager._parseSynctexFromCodeOutput(stdout)) }) }, @@ -466,53 +454,39 @@ module.exports = CompileManager = { const base_dir = Settings.path.synctexBaseDir(compileName) const synctex_path = `${base_dir}/output.pdf` const command = ['pdf', synctex_path, page, h, v] - return fse.ensureDir(compileDir, function(error) { + CompileManager._runSynctex(project_id, user_id, command, function( + error, + stdout + ) { if (error != null) { - logger.err( - { error, project_id, user_id, file_name }, - 'error ensuring dir for sync to code' - ) return callback(error) } - return CompileManager._runSynctex(project_id, user_id, command, function( - error, - stdout - ) { - if (error != null) { - return callback(error) - } - logger.log( - { project_id, user_id, page, h, v, stdout }, - 'synctex pdf output' - ) - return callback( - null, - CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) - ) - }) + logger.log( + { project_id, user_id, page, h, v, stdout }, + 'synctex pdf output' + ) + return callback( + null, + CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) + ) }) }, - _checkFileExists(path, callback) { + _checkFileExists(dir, filename, callback) { if (callback == null) { callback = function(error) {} } - const synctexDir = Path.dirname(path) - const synctexFile = Path.join(synctexDir, 'output.synctex.gz') - return fs.stat(synctexDir, function(error, stats) { + const file = Path.join(dir, filename) + return fs.stat(dir, function(error, stats) { if ((error != null ? error.code : undefined) === 'ENOENT') { - return callback( - new Errors.NotFoundError('called synctex with no output directory') - ) + return callback(new Errors.NotFoundError('no output directory')) } if (error != null) { return callback(error) } - return fs.stat(synctexFile, function(error, stats) { + return fs.stat(file, function(error, stats) { if ((error != null ? error.code : undefined) === 'ENOENT') { - return callback( - new Errors.NotFoundError('called synctex with no output file') - ) + return callback(new Errors.NotFoundError('no output file')) } if (error != null) { return callback(error) @@ -536,24 +510,29 @@ module.exports = CompileManager = { const directory = getCompileDir(project_id, user_id) const timeout = 60 * 1000 // increased to allow for large projects const compileName = getCompileName(project_id, user_id) - return CommandRunner.run( - compileName, - command, - directory, - Settings.clsi != null ? Settings.clsi.docker.image : undefined, - timeout, - {}, - function(error, output) { - if (error != null) { - logger.err( - { err: error, command, project_id, user_id }, - 'error running synctex' - ) - return callback(error) - } - return callback(null, output.stdout) + CompileManager._checkFileExists(directory, 'output.synctex.gz', error => { + if (error) { + return callback(error) } - ) + return CommandRunner.run( + compileName, + command, + directory, + Settings.clsi != null ? Settings.clsi.docker.image : undefined, + timeout, + {}, + function(error, output) { + if (error != null) { + logger.err( + { err: error, command, project_id, user_id }, + 'error running synctex' + ) + return callback(error) + } + return callback(null, output.stdout) + } + ) + }) }, _parseSynctexFromCodeOutput(output) { From 6569da0242bc4cb5e47e88b42218b94c08e4d345 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 12 Jun 2020 15:15:51 +0100 Subject: [PATCH 549/709] use json parsing in request --- test/acceptance/js/helpers/Client.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index 9f430e35..d6594167 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -81,13 +81,14 @@ module.exports = Client = { file, line, column - } + }, + json: true }, (error, response, body) => { if (error != null) { return callback(error) } - return callback(null, JSON.parse(body)) + return callback(null, body) } ) }, @@ -103,13 +104,14 @@ module.exports = Client = { page, h, v - } + }, + json: true }, (error, response, body) => { if (error != null) { return callback(error) } - return callback(null, JSON.parse(body)) + return callback(null, body) } ) }, From 9b92793b89bd74da4cfc8d6a65df7882b7bf6459 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 15 Jun 2020 09:52:21 +0100 Subject: [PATCH 550/709] migrate from underscore to lodash --- app/js/DockerRunner.js | 2 +- app/js/OutputCacheManager.js | 2 +- app/js/OutputFileOptimiser.js | 2 +- app/js/db.js | 2 +- package-lock.json | 10 ---------- package.json | 2 +- test/load/js/loadTest.js | 2 +- test/unit/js/DockerRunnerTests.js | 3 ++- test/unit/js/OutputFileOptimiserTests.js | 3 ++- 9 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index c50087d0..cb6ec2d2 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -25,7 +25,7 @@ const async = require('async') const LockManager = require('./DockerLockManager') const fs = require('fs') const Path = require('path') -const _ = require('underscore') +const _ = require('lodash') const metrics = require('metrics-sharelatex') logger.info('using docker runner') diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index c2c962f1..c0b0d6ed 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -19,7 +19,7 @@ const fs = require('fs') const fse = require('fs-extra') const Path = require('path') const logger = require('logger-sharelatex') -const _ = require('underscore') +const _ = require('lodash') const Settings = require('settings-sharelatex') const crypto = require('crypto') diff --git a/app/js/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js index 80dadab4..e3b3e60e 100644 --- a/app/js/OutputFileOptimiser.js +++ b/app/js/OutputFileOptimiser.js @@ -19,7 +19,7 @@ const Path = require('path') const { spawn } = require('child_process') const logger = require('logger-sharelatex') const Metrics = require('./Metrics') -const _ = require('underscore') +const _ = require('lodash') module.exports = OutputFileOptimiser = { optimiseFile(src, dst, callback) { diff --git a/app/js/db.js b/app/js/db.js index c749af25..15510ae3 100644 --- a/app/js/db.js +++ b/app/js/db.js @@ -10,7 +10,7 @@ */ const Sequelize = require('sequelize') const Settings = require('settings-sharelatex') -const _ = require('underscore') +const _ = require('lodash') const logger = require('logger-sharelatex') const options = _.extend({ logging: false }, Settings.mysql.clsi) diff --git a/package-lock.json b/package-lock.json index 7f896457..63d9c1e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6640,11 +6640,6 @@ "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", "dev": true }, - "underscore": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz", - "integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==" - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -6834,11 +6829,6 @@ "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" }, - "when": { - "version": "3.7.8", - "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", - "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=" - }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index e57bfa55..df30ef77 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "fs-extra": "^8.1.0", "heapdump": "^0.3.15", "lockfile": "^1.0.4", + "lodash": "^4.17.15", "logger-sharelatex": "^1.9.1", "lynx": "0.2.0", "metrics-sharelatex": "^2.6.0", @@ -35,7 +36,6 @@ "sequelize": "^5.21.5", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", "sqlite3": "^4.1.1", - "underscore": "^1.9.2", "v8-profiler-node8": "^6.1.1", "wrench": "~1.5.9" }, diff --git a/test/load/js/loadTest.js b/test/load/js/loadTest.js index ff9850ef..c4116a2a 100644 --- a/test/load/js/loadTest.js +++ b/test/load/js/loadTest.js @@ -13,7 +13,7 @@ const request = require('request') const Settings = require('settings-sharelatex') const async = require('async') const fs = require('fs') -const _ = require('underscore') +const _ = require('lodash') const concurentCompiles = 5 const totalCompiles = 50 diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 1e44daff..b761d203 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -70,7 +70,8 @@ describe('DockerRunner', function() { return runner(callback) } } - } + }, + globals: { Math } // used by lodash }) this.Docker = Docker this.getContainer = Docker.prototype.getContainer diff --git a/test/unit/js/OutputFileOptimiserTests.js b/test/unit/js/OutputFileOptimiserTests.js index b4983bf0..66904414 100644 --- a/test/unit/js/OutputFileOptimiserTests.js +++ b/test/unit/js/OutputFileOptimiserTests.js @@ -30,7 +30,8 @@ describe('OutputFileOptimiser', function() { child_process: { spawn: (this.spawn = sinon.stub()) }, 'logger-sharelatex': { log: sinon.stub(), warn: sinon.stub() }, './Metrics': {} - } + }, + globals: { Math } // used by lodash }) this.directory = '/test/dir' return (this.callback = sinon.stub()) From bad3850fcc615b3dcfca4408eddc3ca996b630c3 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 12 Jun 2020 15:16:14 +0100 Subject: [PATCH 551/709] add acceptance test for synctex when project/file does not exist --- test/acceptance/js/SynctexTests.js | 102 ++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/test/acceptance/js/SynctexTests.js b/test/acceptance/js/SynctexTests.js index 4860c604..1140f3fa 100644 --- a/test/acceptance/js/SynctexTests.js +++ b/test/acceptance/js/SynctexTests.js @@ -69,7 +69,7 @@ Hello world }) }) - return describe('from pdf to code', function() { + describe('from pdf to code', function() { return it('should return the correct location', function(done) { return Client.syncFromPdf( this.project_id, @@ -88,4 +88,104 @@ Hello world ) }) }) + + describe('when the project directory is not available', function() { + before(function() { + this.other_project_id = Client.randomId() + }) + describe('from code to pdf', function() { + it('should return a 404 response', function(done) { + return Client.syncFromCode( + this.other_project_id, + 'main.tex', + 3, + 5, + (error, body) => { + if (error != null) { + throw error + } + expect(body).to.equal('Not Found') + return done() + } + ) + }) + }) + describe('from pdf to code', function() { + it('should return a 404 response', function(done) { + return Client.syncFromPdf( + this.other_project_id, + 1, + 100, + 200, + (error, body) => { + if (error != null) { + throw error + } + expect(body).to.equal('Not Found') + return done() + } + ) + }) + }) + }) + + describe('when the synctex file is not available', function() { + before(function(done) { + this.broken_project_id = Client.randomId() + const content = 'this is not valid tex' // not a valid tex file + this.request = { + resources: [ + { + path: 'main.tex', + content + } + ] + } + Client.compile( + this.broken_project_id, + this.request, + (error, res, body) => { + this.error = error + this.res = res + this.body = body + return done() + } + ) + }) + + describe('from code to pdf', function() { + it('should return a 404 response', function(done) { + return Client.syncFromCode( + this.broken_project_id, + 'main.tex', + 3, + 5, + (error, body) => { + if (error != null) { + throw error + } + expect(body).to.equal('Not Found') + return done() + } + ) + }) + }) + describe('from pdf to code', function() { + it('should return a 404 response', function(done) { + return Client.syncFromPdf( + this.broken_project_id, + 1, + 100, + 200, + (error, body) => { + if (error != null) { + throw error + } + expect(body).to.equal('Not Found') + return done() + } + ) + }) + }) + }) }) From 6c7019ccb7dd3619a3d10faa2874fa43764c5e37 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 15 Jun 2020 11:06:54 +0100 Subject: [PATCH 552/709] downgrade NotFoundError log-level --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index b22e0a01..ff5d70e7 100644 --- a/app.js +++ b/app.js @@ -228,7 +228,7 @@ app.get('/smoke_test_force', (req, res) => smokeTest.sendNewResult(res)) app.use(function(error, req, res, next) { if (error instanceof Errors.NotFoundError) { - logger.warn({ err: error, url: req.url }, 'not found error') + logger.log({ err: error, url: req.url }, 'not found error') return res.sendStatus(404) } else if (error.code === 'EPIPE') { // inspect container returns EPIPE when shutting down From b33734bab64beee8f06327093f9b4bf67c2d5eec Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 11 Jun 2020 16:01:44 +0100 Subject: [PATCH 553/709] add initial compileGroup support --- app/js/CompileManager.js | 7 +++- app/js/DockerRunner.js | 40 ++++++++++++++++-- app/js/LatexRunner.js | 14 ++++++- app/js/LocalCommandRunner.js | 11 ++++- app/js/RequestParser.js | 12 +++++- config/settings.defaults.js | 26 ++++++++++++ test/unit/js/CompileManagerTests.js | 18 ++++---- test/unit/js/DockerRunnerTests.js | 64 +++++++++++++++++++++++++++++ test/unit/js/LatexRunnerTests.js | 9 ++-- 9 files changed, 183 insertions(+), 18 deletions(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 614c49aa..8ca80d8e 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -199,7 +199,8 @@ module.exports = CompileManager = { timeout: request.timeout, image: request.imageName, flags: request.flags, - environment: env + environment: env, + compileGroup: request.compileGroup }, function(error, output, stats, timings) { // request was for validation only @@ -536,6 +537,7 @@ module.exports = CompileManager = { const directory = getCompileDir(project_id, user_id) const timeout = 60 * 1000 // increased to allow for large projects const compileName = getCompileName(project_id, user_id) + const compileGroup = 'synctex' return CommandRunner.run( compileName, command, @@ -543,6 +545,7 @@ module.exports = CompileManager = { Settings.clsi != null ? Settings.clsi.docker.image : undefined, timeout, {}, + compileGroup, function(error, output) { if (error != null) { logger.err( @@ -606,6 +609,7 @@ module.exports = CompileManager = { const compileDir = getCompileDir(project_id, user_id) const timeout = 60 * 1000 const compileName = getCompileName(project_id, user_id) + const compileGroup = 'wordcount' return fse.ensureDir(compileDir, function(error) { if (error != null) { logger.err( @@ -621,6 +625,7 @@ module.exports = CompileManager = { image, timeout, {}, + compileGroup, function(error) { if (error != null) { return callback(error) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index cb6ec2d2..2a6330f8 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -45,7 +45,16 @@ module.exports = DockerRunner = { ERR_EXITED: new Error('exited'), ERR_TIMED_OUT: new Error('container timed out'), - run(project_id, command, directory, image, timeout, environment, callback) { + run( + project_id, + command, + directory, + image, + timeout, + environment, + compileGroup, + callback + ) { let name if (callback == null) { callback = function(error, output) {} @@ -88,7 +97,8 @@ module.exports = DockerRunner = { image, volumes, timeout, - environment + environment, + compileGroup ) const fingerprint = DockerRunner._fingerprintContainer(options) options.name = name = `project-${project_id}-${fingerprint}` @@ -224,7 +234,14 @@ module.exports = DockerRunner = { ) }, - _getContainerOptions(command, image, volumes, timeout, environment) { + _getContainerOptions( + command, + image, + volumes, + timeout, + environment, + compileGroup + ) { let m, year let key, value, hostVol, dockerVol const timeoutInSeconds = timeout / 1000 @@ -311,6 +328,23 @@ module.exports = DockerRunner = { options.HostConfig.Runtime = Settings.clsi.docker.runtime } + if (Settings.clsi.docker.Readonly) { + options.HostConfig.ReadonlyRootfs = true + options.HostConfig.Tmpfs = { '/tmp': 'rw,noexec,nosuid,size=65536k' } + } + + // Allow per-compile group overriding of individual settings + if ( + Settings.clsi.docker.compileGroupConfig && + Settings.clsi.docker.compileGroupConfig[compileGroup] + ) { + const override = Settings.clsi.docker.compileGroupConfig[compileGroup] + let key + for (key in override) { + _.set(options, key, override[key]) + } + } + return options }, diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index c3edb29e..6d1591a2 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -36,7 +36,8 @@ module.exports = LatexRunner = { timeout, image, environment, - flags + flags, + compileGroup } = options if (!compiler) { compiler = 'pdflatex' @@ -46,7 +47,15 @@ module.exports = LatexRunner = { } // milliseconds logger.log( - { directory, compiler, timeout, mainFile, environment, flags }, + { + directory, + compiler, + timeout, + mainFile, + environment, + flags, + compileGroup + }, 'starting compile' ) @@ -79,6 +88,7 @@ module.exports = LatexRunner = { image, timeout, environment, + compileGroup, function(error, output) { delete ProcessTable[id] if (error != null) { diff --git a/app/js/LocalCommandRunner.js b/app/js/LocalCommandRunner.js index ccaf5078..14a19986 100644 --- a/app/js/LocalCommandRunner.js +++ b/app/js/LocalCommandRunner.js @@ -20,7 +20,16 @@ const logger = require('logger-sharelatex') logger.info('using standard command runner') module.exports = CommandRunner = { - run(project_id, command, directory, image, timeout, environment, callback) { + run( + project_id, + command, + directory, + image, + timeout, + environment, + compileGroup, + callback + ) { let key, value if (callback == null) { callback = function(error) {} diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index acfdc668..f2be556a 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -74,7 +74,17 @@ module.exports = RequestParser = { default: [], type: 'object' }) - + if (settings.allowedCompileGroups) { + response.compileGroup = this._parseAttribute( + 'compileGroup', + compile.options.compileGroup, + { + validValues: settings.allowedCompileGroups, + default: '', + type: 'string' + } + ) + } // The syncType specifies whether the request contains all // resources (full) or only those resources to be updated // in-place (incremental). diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 9e44e149..f3bb73b0 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -63,6 +63,17 @@ module.exports = { } } +if (process.env.ALLOWED_COMPILE_GROUPS) { + try { + module.exports.allowedCompileGroups = process.env.ALLOWED_COMPILE_GROUPS.split( + ' ' + ) + } catch (error) { + console.error(error, 'could not apply allowed compile group setting') + process.exit(1) + } +} + if (process.env.DOCKER_RUNNER) { let seccompProfilePath module.exports.clsi = { @@ -82,6 +93,21 @@ if (process.env.DOCKER_RUNNER) { checkProjectsIntervalMs: 10 * 60 * 1000 } + try { + // Override individual docker settings using path-based keys, e.g.: + // compileGroupDockerConfigs = { + // priority: { 'HostConfig.CpuShares': 100 } + // beta: { 'dotted.path.here', 'value'} + // } + const compileGroupConfig = JSON.parse( + process.env.COMPILE_GROUP_DOCKER_CONFIGS || '{}' + ) + module.exports.clsi.docker.compileGroupConfig = compileGroupConfig + } catch (error) { + console.error(error, 'could not apply compile group docker configs') + process.exit(1) + } + try { seccompProfilePath = Path.resolve(__dirname, '../seccomp/clsi-profile.json') module.exports.clsi.docker.seccomp_profile = JSON.stringify( diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index 74a0a47f..90d572b2 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -160,7 +160,8 @@ describe('CompileManager', function() { compiler: (this.compiler = 'pdflatex'), timeout: (this.timeout = 42000), imageName: (this.image = 'example.com/image'), - flags: (this.flags = ['-file-line-error']) + flags: (this.flags = ['-file-line-error']), + compileGroup: (this.compileGroup = 'compile-group') } this.env = {} this.Settings.compileDir = 'compiles' @@ -199,7 +200,8 @@ describe('CompileManager', function() { timeout: this.timeout, image: this.image, flags: this.flags, - environment: this.env + environment: this.env, + compileGroup: this.compileGroup }) .should.equal(true) }) @@ -253,7 +255,8 @@ describe('CompileManager', function() { CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16', CHKTEX_EXIT_ON_ERROR: 1, CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000' - } + }, + compileGroup: this.compileGroup }) .should.equal(true) }) @@ -275,7 +278,8 @@ describe('CompileManager', function() { timeout: this.timeout, image: this.image, flags: this.flags, - environment: this.env + environment: this.env, + compileGroup: this.compileGroup }) .should.equal(true) }) @@ -384,7 +388,7 @@ describe('CompileManager', function() { this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n` this.CommandRunner.run = sinon .stub() - .callsArgWith(6, null, { stdout: this.stdout }) + .callsArgWith(7, null, { stdout: this.stdout }) return this.CompileManager.syncFromCode( this.project_id, this.user_id, @@ -443,7 +447,7 @@ describe('CompileManager', function() { this.stdout = `NODE\t${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}\t${this.line}\t${this.column}\n` this.CommandRunner.run = sinon .stub() - .callsArgWith(6, null, { stdout: this.stdout }) + .callsArgWith(7, null, { stdout: this.stdout }) return this.CompileManager.syncFromPdf( this.project_id, this.user_id, @@ -485,7 +489,7 @@ describe('CompileManager', function() { return describe('wordcount', function() { beforeEach(function() { - this.CommandRunner.run = sinon.stub().callsArg(6) + this.CommandRunner.run = sinon.stub().callsArg(7) this.fs.readFile = sinon .stub() .callsArgWith( diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index b761d203..2e2ffec6 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -87,6 +87,7 @@ describe('DockerRunner', function() { this.project_id = 'project-id-123' this.volumes = { '/local/compile/directory': '/compile' } this.Settings.clsi.docker.image = this.defaultImage = 'default-image' + this.compileGroup = 'compile-group' return (this.Settings.clsi.docker.env = { PATH: 'mock-path' }) }) @@ -123,6 +124,7 @@ describe('DockerRunner', function() { this.image, this.timeout, this.env, + this.compileGroup, (err, output) => { this.callback(err, output) return done() @@ -172,6 +174,7 @@ describe('DockerRunner', function() { this.image, this.timeout, this.env, + this.compileGroup, this.callback ) }) @@ -220,6 +223,7 @@ describe('DockerRunner', function() { this.image, this.timeout, this.env, + this.compileGroup, this.callback ) }) @@ -253,6 +257,7 @@ describe('DockerRunner', function() { null, this.timeout, this.env, + this.compileGroup, this.callback ) }) @@ -282,6 +287,7 @@ describe('DockerRunner', function() { this.image, this.timeout, this.env, + this.compileGroup, this.callback ) }) @@ -293,6 +299,64 @@ describe('DockerRunner', function() { }) }) + describe('run with _getOptions', function() { + beforeEach(function(done) { + // this.DockerRunner._getContainerOptions = sinon + // .stub() + // .returns((this.options = { mockoptions: 'foo' })) + this.DockerRunner._fingerprintContainer = sinon + .stub() + .returns((this.fingerprint = 'fingerprint')) + + this.name = `project-${this.project_id}-${this.fingerprint}` + + this.command = ['mock', 'command', '--outdir=$COMPILE_DIR'] + this.command_with_dir = ['mock', 'command', '--outdir=/compile'] + this.timeout = 42000 + return done() + }) + + describe('when a compile group config is set', function() { + beforeEach(function() { + this.Settings.clsi.docker.compileGroupConfig = { + 'compile-group': { + 'HostConfig.newProperty': 'new-property' + }, + 'other-group': { otherProperty: 'other-property' } + } + this.DockerRunner._runAndWaitForContainer = sinon + .stub() + .callsArgWith(3, null, (this.output = 'mock-output')) + return this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + this.image, + this.timeout, + this.env, + this.compileGroup, + this.callback + ) + }) + + it('should set the docker options for the compile group', function() { + const options = this.DockerRunner._runAndWaitForContainer.lastCall + .args[0] + return expect(options.HostConfig).to.deep.include({ + Binds: ['/local/compile/directory:/compile:rw'], + LogConfig: { Type: 'none', Config: {} }, + CapDrop: 'ALL', + SecurityOpt: ['no-new-privileges'], + newProperty: 'new-property' + }) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null, this.output).should.equal(true) + }) + }) + }) + describe('_runAndWaitForContainer', function() { beforeEach(function() { this.options = { mockoptions: 'foo', name: (this.name = 'mock-name') } diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index b112b170..f480bc82 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -48,6 +48,7 @@ describe('LatexRunner', function() { this.mainFile = 'main-file.tex' this.compiler = 'pdflatex' this.image = 'example.com/image' + this.compileGroup = 'compile-group' this.callback = sinon.stub() this.project_id = 'project-id-123' return (this.env = { foo: '123' }) @@ -55,7 +56,7 @@ describe('LatexRunner', function() { return describe('runLatex', function() { beforeEach(function() { - return (this.CommandRunner.run = sinon.stub().callsArgWith(6, null, { + return (this.CommandRunner.run = sinon.stub().callsArgWith(7, null, { stdout: 'this is stdout', stderr: 'this is stderr' })) @@ -71,7 +72,8 @@ describe('LatexRunner', function() { compiler: this.compiler, timeout: (this.timeout = 42000), image: this.image, - environment: this.env + environment: this.env, + compileGroup: this.compileGroup }, this.callback ) @@ -85,7 +87,8 @@ describe('LatexRunner', function() { this.directory, this.image, this.timeout, - this.env + this.env, + this.compileGroup ) .should.equal(true) }) From a88000281f2c8f6fa0ab61f42bd1ee4ac5f51bfc Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 15 Jun 2020 15:49:38 +0100 Subject: [PATCH 554/709] add default settings to remove wordcount and synctex containers --- config/settings.defaults.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/settings.defaults.js b/config/settings.defaults.js index f3bb73b0..3e115e8b 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -102,7 +102,15 @@ if (process.env.DOCKER_RUNNER) { const compileGroupConfig = JSON.parse( process.env.COMPILE_GROUP_DOCKER_CONFIGS || '{}' ) - module.exports.clsi.docker.compileGroupConfig = compileGroupConfig + // Automatically clean up wordcount and synctex containers + const defaultCompileGroupConfig = { + wordcount: { 'HostConfig.AutoRemove': true }, + synctex: { 'HostConfig.AutoRemove': true } + } + module.exports.clsi.docker.compileGroupConfig = Object.assign( + defaultCompileGroupConfig, + compileGroupConfig + ) } catch (error) { console.error(error, 'could not apply compile group docker configs') process.exit(1) From 74a11c7be31d5ca385a9aed8a20d48ea4a8f98cb Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 16 Jun 2020 08:45:53 +0100 Subject: [PATCH 555/709] fix format --- app/js/DockerRunner.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index e75b997d..21ea97f0 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -27,7 +27,6 @@ const fs = require('fs') const Path = require('path') const _ = require('lodash') - logger.info('using docker runner') const usingSiblingContainers = () => From b1ca08fd0c632e07f84bd0ab6c8ddca36b2a0ab1 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 18 Jun 2020 09:54:18 +0100 Subject: [PATCH 556/709] handle EPIPE errors in CompileController --- app/js/CompileController.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/js/CompileController.js b/app/js/CompileController.js index e146b62c..c76d0d50 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -55,6 +55,10 @@ module.exports = CompileController = { } else if (error instanceof Errors.FilesOutOfSyncError) { code = 409 // Http 409 Conflict status = 'retry' + } else if (error && error.code === 'EPIPE') { + // docker returns EPIPE when shutting down + code = 503 // send 503 Unavailable response + status = 'unavailable' } else if (error != null ? error.terminated : undefined) { status = 'terminated' } else if (error != null ? error.validate : undefined) { From ad8fec6a1a44466e0bcc0dff5964778d7428dba9 Mon Sep 17 00:00:00 2001 From: Miguel Serrano <mserranom@gmail.com> Date: Thu, 25 Jun 2020 12:31:10 +0200 Subject: [PATCH 557/709] Fixed NPE when Settings.clsi is defined but Settings.clsi.docker is not --- app/js/CompileManager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 8cd7797a..73db6499 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -520,7 +520,9 @@ module.exports = CompileManager = { compileName, command, directory, - Settings.clsi != null ? Settings.clsi.docker.image : undefined, + Settings.clsi && Settings.clsi.docker + ? Settings.clsi.docker.image + : undefined, timeout, {}, compileGroup, From 5ed09d1a980e8feaca0a859697a549b86abef3b5 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Fri, 26 Jun 2020 12:29:49 +0100 Subject: [PATCH 558/709] [misc] RequestParser: restrict imageName to an allow list and add tests --- app/js/RequestParser.js | 2 +- config/settings.defaults.js | 10 ++++ docker-compose-config.yml | 2 + test/acceptance/js/AllowedImageNames.js | 73 +++++++++++++++++++++++++ test/unit/js/RequestParserTests.js | 38 +++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 test/acceptance/js/AllowedImageNames.js diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index f2be556a..ecc92c08 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -61,7 +61,7 @@ module.exports = RequestParser = { response.imageName = this._parseAttribute( 'imageName', compile.options.imageName, - { type: 'string' } + { type: 'string', validValues: settings.allowedImageNamesFlat } ) response.draft = this._parseAttribute('draft', compile.options.draft, { default: false, diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 3e115e8b..fd2c28af 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -73,6 +73,16 @@ if (process.env.ALLOWED_COMPILE_GROUPS) { process.exit(1) } } +if (process.env.ALLOWED_IMAGE_NAMES_FLAT) { + try { + module.exports.allowedImageNamesFlat = process.env.ALLOWED_IMAGE_NAMES_FLAT.split( + ' ' + ) + } catch (error) { + console.error(error, 'could not apply allowed image names setting') + process.exit(1) + } +} if (process.env.DOCKER_RUNNER) { let seccompProfilePath diff --git a/docker-compose-config.yml b/docker-compose-config.yml index 392f8fea..d1b72ee2 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -3,6 +3,7 @@ version: "2.3" services: dev: environment: + ALLOWED_IMAGE_NAMES_FLAT: "quay.io/sharelatex/texlive-full:2017.1" TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: "tex" SHARELATEX_CONFIG: /app/config/settings.defaults.coffee @@ -18,6 +19,7 @@ services: ci: environment: + ALLOWED_IMAGE_NAMES_FLAT: ${TEXLIVE_IMAGE} TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: "tex" SHARELATEX_CONFIG: /app/config/settings.defaults.coffee diff --git a/test/acceptance/js/AllowedImageNames.js b/test/acceptance/js/AllowedImageNames.js new file mode 100644 index 00000000..1ea0a36b --- /dev/null +++ b/test/acceptance/js/AllowedImageNames.js @@ -0,0 +1,73 @@ +const Client = require('./helpers/Client') +const ClsiApp = require('./helpers/ClsiApp') +const { expect } = require('chai') + +describe('AllowedImageNames', function() { + beforeEach(function(done) { + this.project_id = Client.randomId() + this.request = { + options: { + imageName: undefined + }, + resources: [ + { + path: 'main.tex', + content: `\ +\\documentclass{article} +\\begin{document} +Hello world +\\end{document}\ +` + } + ] + } + ClsiApp.ensureRunning(done) + }) + + describe('with a valid name', function() { + beforeEach(function(done) { + this.request.options.imageName = process.env.TEXLIVE_IMAGE + + Client.compile(this.project_id, this.request, (error, res, body) => { + this.error = error + this.res = res + this.body = body + done(error) + }) + }) + it('should return success', function() { + expect(this.res.statusCode).to.equal(200) + }) + + it('should return a PDF', function() { + let pdf + try { + pdf = Client.getOutputFile(this.body, 'pdf') + } catch (e) {} + expect(pdf).to.exist + }) + }) + + describe('with an invalid name', function() { + beforeEach(function(done) { + this.request.options.imageName = 'something/evil:1337' + Client.compile(this.project_id, this.request, (error, res, body) => { + this.error = error + this.res = res + this.body = body + done(error) + }) + }) + it('should return non success', function() { + expect(this.res.statusCode).to.not.equal(200) + }) + + it('should not return a PDF', function() { + let pdf + try { + pdf = Client.getOutputFile(this.body, 'pdf') + } catch (e) {} + expect(pdf).to.not.exist + }) + }) +}) diff --git a/test/unit/js/RequestParserTests.js b/test/unit/js/RequestParserTests.js index e2d8b026..16955bb6 100644 --- a/test/unit/js/RequestParserTests.js +++ b/test/unit/js/RequestParserTests.js @@ -114,6 +114,44 @@ describe('RequestParser', function() { }) }) + describe('when image restrictions are present', function() { + beforeEach(function() { + this.settings.allowedImageNamesFlat = ['repo/name:tag1', 'repo/name:tag2'] + }) + + describe('with imageName set to something invalid', function() { + beforeEach(function() { + const request = this.validRequest + request.compile.options.imageName = 'something/different:latest' + this.RequestParser.parse(request, (error, data) => { + this.error = error + this.data = data + }) + }) + + it('should throw an error for imageName', function() { + expect(String(this.error)).to.include( + 'imageName attribute should be one of' + ) + }) + }) + + describe('with imageName set to something valid', function() { + beforeEach(function() { + const request = this.validRequest + request.compile.options.imageName = 'repo/name:tag1' + this.RequestParser.parse(request, (error, data) => { + this.error = error + this.data = data + }) + }) + + it('should set the imageName', function() { + this.data.imageName.should.equal('repo/name:tag1') + }) + }) + }) + describe('with flags set', function() { beforeEach(function() { this.validRequest.compile.options.flags = ['-file-line-error'] From 6edb4589108c51719f0a368767eb49a71e7b1250 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Fri, 26 Jun 2020 13:17:45 +0100 Subject: [PATCH 559/709] [misc] wordcount: restrict image to an allow list and add tests --- app/js/CompileController.js | 7 ++++ test/acceptance/js/AllowedImageNames.js | 29 +++++++++++++++++ test/acceptance/js/helpers/Client.js | 9 ++++++ test/unit/js/CompileControllerTests.js | 43 +++++++++++++++++++++++-- 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/app/js/CompileController.js b/app/js/CompileController.js index c76d0d50..857d0504 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -218,6 +218,13 @@ module.exports = CompileController = { const { project_id } = req.params const { user_id } = req.params const { image } = req.query + if ( + image && + Settings.allowedImageNamesFlat && + Settings.allowedImageNamesFlat.indexOf(image) === -1 + ) { + return res.status(400).send('invalid image') + } logger.log({ image, file, project_id }, 'word count request') return CompileManager.wordcount(project_id, user_id, file, image, function( diff --git a/test/acceptance/js/AllowedImageNames.js b/test/acceptance/js/AllowedImageNames.js index 1ea0a36b..a9b3996e 100644 --- a/test/acceptance/js/AllowedImageNames.js +++ b/test/acceptance/js/AllowedImageNames.js @@ -70,4 +70,33 @@ Hello world expect(pdf).to.not.exist }) }) + + describe('wordcount', function() { + beforeEach(function(done) { + Client.compile(this.project_id, this.request, done) + }) + it('should error out with an invalid imageName', function() { + Client.wordcountWithImage( + this.project_id, + 'main.tex', + 'something/evil:1337', + (error, result) => { + expect(String(error)).to.include('statusCode=400') + } + ) + }) + + it('should produce a texcout a valid imageName', function() { + Client.wordcountWithImage( + this.project_id, + 'main.tex', + process.env.TEXLIVE_IMAGE, + (error, result) => { + expect(error).to.not.exist + expect(result).to.exist + expect(result.texcount).to.exist + } + ) + }) + }) }) diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index d6594167..c940e305 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -189,6 +189,11 @@ module.exports = Client = { }, wordcount(project_id, file, callback) { + const image = undefined + Client.wordcountWithImage(project_id, file, image, callback) + }, + + wordcountWithImage(project_id, file, image, callback) { if (callback == null) { callback = function(error, pdfPositions) {} } @@ -196,6 +201,7 @@ module.exports = Client = { { url: `${this.host}/project/${project_id}/wordcount`, qs: { + image, file } }, @@ -203,6 +209,9 @@ module.exports = Client = { if (error != null) { return callback(error) } + if (response.statusCode !== 200) { + return callback(new Error(`statusCode=${response.statusCode}`)) + } return callback(null, JSON.parse(body)) } ) diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 4480c880..f3f3fa4a 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -12,6 +12,7 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') require('chai').should() +const { expect } = require('chai') const modulePath = require('path').join( __dirname, '../../../app/js/CompileController' @@ -287,21 +288,59 @@ describe('CompileController', function() { this.CompileManager.wordcount = sinon .stub() .callsArgWith(4, null, (this.texcount = ['mock-texcount'])) - return this.CompileController.wordcount(this.req, this.res, this.next) }) it('should return the word count of a file', function() { + this.CompileController.wordcount(this.req, this.res, this.next) return this.CompileManager.wordcount .calledWith(this.project_id, undefined, this.file, this.image) .should.equal(true) }) - return it('should return the texcount info', function() { + it('should return the texcount info', function() { + this.CompileController.wordcount(this.req, this.res, this.next) return this.res.json .calledWith({ texcount: this.texcount }) .should.equal(true) }) + + describe('when allowedImageNamesFlat is set', function() { + beforeEach(function() { + this.Settings.allowedImageNamesFlat = [ + 'repo/image:tag1', + 'repo/image:tag2' + ] + this.res.send = sinon.stub() + this.res.status = sinon.stub().returns({ send: this.res.send }) + }) + + describe('with an invalid image', function() { + beforeEach(function() { + this.req.query.image = 'something/evil:1337' + this.CompileController.wordcount(this.req, this.res, this.next) + }) + it('should return a 400', function() { + expect(this.res.status.calledWith(400)).to.equal(true) + }) + it('should not run the query', function() { + expect(this.CompileManager.wordcount.called).to.equal(false) + }) + }) + + describe('with a valid image', function() { + beforeEach(function() { + this.req.query.image = 'repo/image:tag1' + this.CompileController.wordcount(this.req, this.res, this.next) + }) + it('should not return a 400', function() { + expect(this.res.status.calledWith(400)).to.equal(false) + }) + it('should run the query', function() { + expect(this.CompileManager.wordcount.called).to.equal(true) + }) + }) + }) }) }) From ee0e8066d3c6d6e3f4041e2364c631fe451558be Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 30 Jun 2020 12:00:18 +0100 Subject: [PATCH 560/709] [misc] apply review feedback - move setting into clsi.docker namespace - rename the variable for images to allowedImages / ALLOWED_IMAGES - add an additional check for the image name into the DockerRunner Co-Authored-By: Brian Gough <brian.gough@overleaf.com> --- app/js/CompileController.js | 6 ++- app/js/DockerRunner.js | 7 ++++ app/js/RequestParser.js | 8 +++- config/settings.defaults.js | 21 +++++----- docker-compose-config.yml | 4 +- test/unit/js/CompileControllerTests.js | 5 ++- test/unit/js/DockerRunnerTests.js | 58 +++++++++++++++++++++++++- test/unit/js/RequestParserTests.js | 6 ++- 8 files changed, 96 insertions(+), 19 deletions(-) diff --git a/app/js/CompileController.js b/app/js/CompileController.js index 857d0504..9c18830d 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -220,8 +220,10 @@ module.exports = CompileController = { const { image } = req.query if ( image && - Settings.allowedImageNamesFlat && - Settings.allowedImageNamesFlat.indexOf(image) === -1 + Settings.clsi && + Settings.clsi.docker && + Settings.clsi.docker.allowedImages && + !Settings.clsi.docker.allowedImages.includes(image) ) { return res.status(400).send('invalid image') } diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 5874a51f..5f04fe0c 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -91,6 +91,13 @@ module.exports = DockerRunner = { image = `${Settings.texliveImageNameOveride}/${img[2]}` } + if ( + Settings.clsi.docker.allowedImages && + !Settings.clsi.docker.allowedImages.includes(image) + ) { + return callback(new Error('image not allowed')) + } + const options = DockerRunner._getContainerOptions( command, image, diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index ecc92c08..342dbc86 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -61,7 +61,13 @@ module.exports = RequestParser = { response.imageName = this._parseAttribute( 'imageName', compile.options.imageName, - { type: 'string', validValues: settings.allowedImageNamesFlat } + { + type: 'string', + validValues: + settings.clsi && + settings.clsi.docker && + settings.clsi.docker.allowedImages + } ) response.draft = this._parseAttribute('draft', compile.options.draft, { default: false, diff --git a/config/settings.defaults.js b/config/settings.defaults.js index fd2c28af..823f1f73 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -73,16 +73,6 @@ if (process.env.ALLOWED_COMPILE_GROUPS) { process.exit(1) } } -if (process.env.ALLOWED_IMAGE_NAMES_FLAT) { - try { - module.exports.allowedImageNamesFlat = process.env.ALLOWED_IMAGE_NAMES_FLAT.split( - ' ' - ) - } catch (error) { - console.error(error, 'could not apply allowed image names setting') - process.exit(1) - } -} if (process.env.DOCKER_RUNNER) { let seccompProfilePath @@ -139,6 +129,17 @@ if (process.env.DOCKER_RUNNER) { process.exit(1) } + if (process.env.ALLOWED_IMAGES) { + try { + module.exports.clsi.docker.allowedImages = process.env.ALLOWED_IMAGES.split( + ' ' + ) + } catch (error) { + console.error(error, 'could not apply allowed images setting') + process.exit(1) + } + } + module.exports.path.synctexBaseDir = () => '/compile' module.exports.path.sandboxedCompilesHostDir = process.env.COMPILES_HOST_DIR diff --git a/docker-compose-config.yml b/docker-compose-config.yml index d1b72ee2..afe56bbe 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -3,7 +3,7 @@ version: "2.3" services: dev: environment: - ALLOWED_IMAGE_NAMES_FLAT: "quay.io/sharelatex/texlive-full:2017.1" + ALLOWED_IMAGES: "quay.io/sharelatex/texlive-full:2017.1" TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: "tex" SHARELATEX_CONFIG: /app/config/settings.defaults.coffee @@ -19,7 +19,7 @@ services: ci: environment: - ALLOWED_IMAGE_NAMES_FLAT: ${TEXLIVE_IMAGE} + ALLOWED_IMAGES: ${TEXLIVE_IMAGE} TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: "tex" SHARELATEX_CONFIG: /app/config/settings.defaults.coffee diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index f3f3fa4a..8bb83e66 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -306,9 +306,10 @@ describe('CompileController', function() { .should.equal(true) }) - describe('when allowedImageNamesFlat is set', function() { + describe('when allowedImages is set', function() { beforeEach(function() { - this.Settings.allowedImageNamesFlat = [ + this.Settings.clsi = { docker: {} } + this.Settings.clsi.docker.allowedImages = [ 'repo/image:tag1', 'repo/image:tag2' ] diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 16ecbbc5..9c1731a8 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -273,7 +273,7 @@ describe('DockerRunner', function() { }) }) - return describe('with image override', function() { + describe('with image override', function() { beforeEach(function() { this.Settings.texliveImageNameOveride = 'overrideimage.com/something' this.DockerRunner._runAndWaitForContainer = sinon @@ -296,6 +296,62 @@ describe('DockerRunner', function() { return image.should.equal('overrideimage.com/something/image:2016.2') }) }) + + describe('with image restriction', function() { + beforeEach(function() { + this.Settings.clsi.docker.allowedImages = [ + 'repo/image:tag1', + 'repo/image:tag2' + ] + this.DockerRunner._runAndWaitForContainer = sinon + .stub() + .callsArgWith(3, null, (this.output = 'mock-output')) + }) + + describe('with a valid image', function() { + beforeEach(function() { + this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + 'repo/image:tag1', + this.timeout, + this.env, + this.compileGroup, + this.callback + ) + }) + + it('should setup the container', function() { + this.DockerRunner._getContainerOptions.called.should.equal(true) + }) + }) + + describe('with a invalid image', function() { + beforeEach(function() { + this.DockerRunner.run( + this.project_id, + this.command, + this.directory, + 'something/different:evil', + this.timeout, + this.env, + this.compileGroup, + this.callback + ) + }) + + it('should call the callback with an error', function() { + const err = new Error('image not allowed') + this.callback.called.should.equal(true) + this.callback.args[0][0].message.should.equal(err.message) + }) + + it('should not setup the container', function() { + this.DockerRunner._getContainerOptions.called.should.equal(false) + }) + }) + }) }) describe('run with _getOptions', function() { diff --git a/test/unit/js/RequestParserTests.js b/test/unit/js/RequestParserTests.js index 16955bb6..25b6b293 100644 --- a/test/unit/js/RequestParserTests.js +++ b/test/unit/js/RequestParserTests.js @@ -116,7 +116,11 @@ describe('RequestParser', function() { describe('when image restrictions are present', function() { beforeEach(function() { - this.settings.allowedImageNamesFlat = ['repo/name:tag1', 'repo/name:tag2'] + this.settings.clsi = { docker: {} } + this.settings.clsi.docker.allowedImages = [ + 'repo/name:tag1', + 'repo/name:tag2' + ] }) describe('with imageName set to something invalid', function() { From 0cecf2656981141e8951ac8aafca6a1c07c972a9 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 1 Jul 2020 10:01:25 +0100 Subject: [PATCH 561/709] [misc] move the image check prior to the base image override --- app/js/DockerRunner.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 5f04fe0c..49c7f402 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -86,11 +86,6 @@ module.exports = DockerRunner = { ;({ image } = Settings.clsi.docker) } - if (Settings.texliveImageNameOveride != null) { - const img = image.split('/') - image = `${Settings.texliveImageNameOveride}/${img[2]}` - } - if ( Settings.clsi.docker.allowedImages && !Settings.clsi.docker.allowedImages.includes(image) @@ -98,6 +93,11 @@ module.exports = DockerRunner = { return callback(new Error('image not allowed')) } + if (Settings.texliveImageNameOveride != null) { + const img = image.split('/') + image = `${Settings.texliveImageNameOveride}/${img[2]}` + } + const options = DockerRunner._getContainerOptions( command, image, From 267ff9e7f1c448b188bdd38022fa3c22a8504227 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Tue, 23 Apr 2019 01:00:46 +0200 Subject: [PATCH 562/709] [ExampleDocumentTests] drop out in case of an error during compilation Signed-off-by: Jakob Ackermann <das7pad@outlook.com> --- test/acceptance/js/ExampleDocumentTests.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/acceptance/js/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js index 0134c0e1..0a544795 100644 --- a/test/acceptance/js/ExampleDocumentTests.js +++ b/test/acceptance/js/ExampleDocumentTests.js @@ -235,6 +235,7 @@ describe('Example Documents', function() { ) === 'failure' ) { console.log('DEBUG: error', error, 'body', JSON.stringify(body)) + return done(new Error("Compile failed")) } const pdf = Client.getOutputFile(body, 'pdf') return downloadAndComparePdf( @@ -263,6 +264,7 @@ describe('Example Documents', function() { ) === 'failure' ) { console.log('DEBUG: error', error, 'body', JSON.stringify(body)) + return done(new Error("Compile failed")) } const pdf = Client.getOutputFile(body, 'pdf') return downloadAndComparePdf( From 53cc80fc7f0ad16bc001f128ea71db87524a9efa Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Fri, 3 Jul 2020 11:47:53 +0100 Subject: [PATCH 563/709] [misc] fix formatting --- test/acceptance/js/ExampleDocumentTests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/acceptance/js/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js index 0a544795..5435a787 100644 --- a/test/acceptance/js/ExampleDocumentTests.js +++ b/test/acceptance/js/ExampleDocumentTests.js @@ -235,7 +235,7 @@ describe('Example Documents', function() { ) === 'failure' ) { console.log('DEBUG: error', error, 'body', JSON.stringify(body)) - return done(new Error("Compile failed")) + return done(new Error('Compile failed')) } const pdf = Client.getOutputFile(body, 'pdf') return downloadAndComparePdf( @@ -264,7 +264,7 @@ describe('Example Documents', function() { ) === 'failure' ) { console.log('DEBUG: error', error, 'body', JSON.stringify(body)) - return done(new Error("Compile failed")) + return done(new Error('Compile failed')) } const pdf = Client.getOutputFile(body, 'pdf') return downloadAndComparePdf( From f4561c2fe2c84bd363d4efec20786994ca946211 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 10 Aug 2020 17:01:11 +0100 Subject: [PATCH 564/709] [misc] bump the dev-env to 3.3.2 --- .eslintrc | 2 +- .github/dependabot.yml | 17 ++ .gitignore | 3 + Dockerfile | 4 +- Jenkinsfile | 131 --------- Makefile | 6 +- app.js | 63 ++-- app/js/CompileController.js | 52 ++-- app/js/CompileManager.js | 92 +++--- app/js/DockerLockManager.js | 14 +- app/js/DockerRunner.js | 106 +++---- app/js/DraftModeManager.js | 4 +- app/js/Errors.js | 6 +- app/js/LatexRunner.js | 22 +- app/js/LocalCommandRunner.js | 12 +- app/js/LockManager.js | 8 +- app/js/OutputCacheManager.js | 69 ++--- app/js/OutputFileFinder.js | 18 +- app/js/OutputFileOptimiser.js | 18 +- app/js/ProjectPersistenceManager.js | 46 +-- app/js/RequestParser.js | 2 +- app/js/ResourceStateManager.js | 24 +- app/js/ResourceWriter.js | 68 ++--- app/js/SafeReader.js | 8 +- app/js/StaticServerForbidSymlinks.js | 8 +- app/js/TikzManager.js | 15 +- app/js/UrlCache.js | 73 +++-- app/js/UrlFetcher.js | 20 +- app/js/db.js | 2 +- buildscript.txt | 4 +- docker-compose.ci.yml | 2 + docker-compose.yml | 6 +- nodemon.json | 1 - package-lock.json | 12 +- package.json | 4 +- test/acceptance/js/AllowedImageNames.js | 28 +- test/acceptance/js/BrokenLatexFileTests.js | 16 +- test/acceptance/js/DeleteOldFilesTest.js | 16 +- test/acceptance/js/ExampleDocumentTests.js | 194 +++++++------ test/acceptance/js/SimpleLatexFileTests.js | 12 +- test/acceptance/js/SynctexTests.js | 36 +-- test/acceptance/js/TimeoutTests.js | 12 +- test/acceptance/js/UrlCachingTests.js | 56 ++-- test/acceptance/js/WordcountTests.js | 8 +- test/acceptance/js/helpers/Client.js | 20 +- test/acceptance/js/helpers/ClsiApp.js | 6 +- test/load/js/loadTest.js | 10 +- test/smoke/js/SmokeTests.js | 6 +- test/unit/js/CompileControllerTests.js | 84 +++--- test/unit/js/CompileManagerTests.js | 112 ++++---- test/unit/js/ContentTypeMapperTests.js | 30 +- test/unit/js/DockerLockManagerTests.js | 68 ++--- test/unit/js/DockerRunnerTests.js | 272 +++++++++--------- test/unit/js/DraftModeManagerTests.js | 20 +- test/unit/js/LatexRunnerTests.js | 30 +- test/unit/js/LockManagerTests.js | 24 +- test/unit/js/OutputFileFinderTests.js | 26 +- test/unit/js/OutputFileOptimiserTests.js | 65 ++--- .../unit/js/ProjectPersistenceManagerTests.js | 36 +-- test/unit/js/RequestParserTests.js | 180 ++++++------ test/unit/js/ResourceStateManagerTests.js | 62 ++-- test/unit/js/ResourceWriterTests.js | 134 ++++----- .../js/StaticServerForbidSymlinksTests.js | 92 +++--- test/unit/js/TikzManager.js | 54 ++-- test/unit/js/UrlCacheTests.js | 112 ++++---- test/unit/js/UrlFetcherTests.js | 66 ++--- 66 files changed, 1371 insertions(+), 1458 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 Jenkinsfile diff --git a/.eslintrc b/.eslintrc index 2e945d6f..76dad156 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,7 @@ "prettier/standard" ], "parserOptions": { - "ecmaVersion": 2017 + "ecmaVersion": 2018 }, "plugins": [ "mocha", diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c6f98d84 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + + pull-request-branch-name: + # Separate sections of the branch name with a hyphen + # Docker images use the branch name and do not support slashes in tags + # https://github.com/overleaf/google-ops/issues/822 + # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#pull-request-branch-nameseparator + separator: "-" + + # Block informal upgrades -- security upgrades use a separate queue. + # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#open-pull-requests-limit + open-pull-requests-limit: 0 diff --git a/.gitignore b/.gitignore index 912e3801..b32ea209 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ db.sqlite-wal db.sqlite-shm config/* npm-debug.log + +# managed by dev-environment$ bin/update_build_scripts +.npmrc diff --git a/Dockerfile b/Dockerfile index 9b378831..bbf1efd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,12 +15,10 @@ FROM base as app #wildcard as some files may not be in all repos COPY package*.json npm-shrink*.json /app/ -RUN npm install --quiet +RUN npm ci --quiet COPY . /app - - FROM base COPY --from=app /app /app diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index c7b961eb..00000000 --- a/Jenkinsfile +++ /dev/null @@ -1,131 +0,0 @@ -String cron_string = BRANCH_NAME == "master" ? "@daily" : "" - -pipeline { - agent any - - environment { - GIT_PROJECT = "clsi" - JENKINS_WORKFLOW = "clsi-sharelatex" - TARGET_URL = "${env.JENKINS_URL}blue/organizations/jenkins/${JENKINS_WORKFLOW}/detail/$BRANCH_NAME/$BUILD_NUMBER/pipeline" - GIT_API_URL = "https://api.github.com/repos/overleaf/${GIT_PROJECT}/statuses/$GIT_COMMIT" - } - - triggers { - pollSCM('* * * * *') - cron(cron_string) - } - - stages { - - stage('Install') { - steps { - withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { - sh "curl $GIT_API_URL \ - --data '{ \ - \"state\" : \"pending\", \ - \"target_url\": \"$TARGET_URL\", \ - \"description\": \"Your build is underway\", \ - \"context\": \"ci/jenkins\" }' \ - -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" - } - } - } - - stage('Build') { - steps { - sh 'make build' - } - } - - stage('Linting') { - steps { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make format' - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make lint' - } - } - - stage('Unit Tests') { - steps { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit' - } - } - - stage('Acceptance Tests') { - steps { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_acceptance' - } - } - - stage('Package and docker push') { - steps { - sh 'echo ${BUILD_NUMBER} > build_number.txt' - sh 'touch build.tar.gz' // Avoid tar warning about files changing during read - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make tar' - - withCredentials([file(credentialsId: 'gcr.io_overleaf-ops', variable: 'DOCKER_REPO_KEY_PATH')]) { - sh 'docker login -u _json_key --password-stdin https://gcr.io/overleaf-ops < ${DOCKER_REPO_KEY_PATH}' - } - sh 'DOCKER_REPO=gcr.io/overleaf-ops make publish' - sh 'docker logout https://gcr.io/overleaf-ops' - - } - } - - stage('Publish to s3') { - steps { - sh 'echo ${BRANCH_NAME}-${BUILD_NUMBER} > build_number.txt' - withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { - s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") - } - withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { - // The deployment process uses this file to figure out the latest build - s3Upload(file:'build_number.txt', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/latest") - } - } - } - } - - post { - always { - sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_clean' - sh 'make clean' - } - - success { - withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { - sh "curl $GIT_API_URL \ - --data '{ \ - \"state\" : \"success\", \ - \"target_url\": \"$TARGET_URL\", \ - \"description\": \"Your build succeeded!\", \ - \"context\": \"ci/jenkins\" }' \ - -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" - } - } - - failure { - mail(from: "${EMAIL_ALERT_FROM}", - to: "${EMAIL_ALERT_TO}", - subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", - body: "Build: ${BUILD_URL}") - withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) { - sh "curl $GIT_API_URL \ - --data '{ \ - \"state\" : \"failure\", \ - \"target_url\": \"$TARGET_URL\", \ - \"description\": \"Your build failed\", \ - \"context\": \"ci/jenkins\" }' \ - -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD" - } - } - } - - // The options directive is for configuration that applies to the whole job. - options { - // we'd like to make sure remove old builds, so we don't fill up our storage! - buildDiscarder(logRotator(numToKeepStr:'50')) - - // And we'd really like to be sure that this build doesn't hang forever, so let's time it out after: - timeout(time: 30, unit: 'MINUTES') - } -} diff --git a/Makefile b/Makefile index c938f87a..040a9315 100644 --- a/Makefile +++ b/Makefile @@ -25,13 +25,13 @@ clean: docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) format: - $(DOCKER_COMPOSE) run --rm test_unit npm run format + $(DOCKER_COMPOSE) run --rm test_unit npm run --silent format format_fix: - $(DOCKER_COMPOSE) run --rm test_unit npm run format:fix + $(DOCKER_COMPOSE) run --rm test_unit npm run --silent format:fix lint: - $(DOCKER_COMPOSE) run --rm test_unit npm run lint + $(DOCKER_COMPOSE) run --rm test_unit npm run --silent lint test: format lint test_unit test_acceptance diff --git a/app.js b/app.js index ff5d70e7..7ebca04e 100644 --- a/app.js +++ b/app.js @@ -42,14 +42,14 @@ app.use(Metrics.http.monitor(logger)) // minutes (including file download time), so bump up the // timeout a bit. const TIMEOUT = 10 * 60 * 1000 -app.use(function(req, res, next) { +app.use(function (req, res, next) { req.setTimeout(TIMEOUT) res.setTimeout(TIMEOUT) res.removeHeader('X-Powered-By') return next() }) -app.param('project_id', function(req, res, next, projectId) { +app.param('project_id', function (req, res, next, projectId) { if (projectId != null ? projectId.match(/^[a-zA-Z0-9_-]+$/) : undefined) { return next() } else { @@ -57,7 +57,7 @@ app.param('project_id', function(req, res, next, projectId) { } }) -app.param('user_id', function(req, res, next, userId) { +app.param('user_id', function (req, res, next, userId) { if (userId != null ? userId.match(/^[0-9a-f]{24}$/) : undefined) { return next() } else { @@ -65,7 +65,7 @@ app.param('user_id', function(req, res, next, userId) { } }) -app.param('build_id', function(req, res, next, buildId) { +app.param('build_id', function (req, res, next, buildId) { if ( buildId != null ? buildId.match(OutputCacheManager.BUILD_REGEX) : undefined ) { @@ -134,19 +134,18 @@ const staticServer = ForbidSymlinks(express.static, Settings.path.compilesDir, { } }) -app.get('/project/:project_id/user/:user_id/build/:build_id/output/*', function( - req, - res, - next -) { - // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = - `/${req.params.project_id}-${req.params.user_id}/` + - OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) - return staticServer(req, res, next) -}) +app.get( + '/project/:project_id/user/:user_id/build/:build_id/output/*', + function (req, res, next) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = + `/${req.params.project_id}-${req.params.user_id}/` + + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) + return staticServer(req, res, next) + } +) -app.get('/project/:project_id/build/:build_id/output/*', function( +app.get('/project/:project_id/build/:build_id/output/*', function ( req, res, next @@ -158,7 +157,7 @@ app.get('/project/:project_id/build/:build_id/output/*', function( return staticServer(req, res, next) }) -app.get('/project/:project_id/user/:user_id/output/*', function( +app.get('/project/:project_id/user/:user_id/output/*', function ( req, res, next @@ -168,7 +167,7 @@ app.get('/project/:project_id/user/:user_id/output/*', function( return staticServer(req, res, next) }) -app.get('/project/:project_id/output/*', function(req, res, next) { +app.get('/project/:project_id/output/*', function (req, res, next) { if ( (req.query != null ? req.query.build : undefined) != null && req.query.build.match(OutputCacheManager.BUILD_REGEX) @@ -183,7 +182,7 @@ app.get('/project/:project_id/output/*', function(req, res, next) { return staticServer(req, res, next) }) -app.get('/oops', function(req, res, next) { +app.get('/oops', function (req, res, next) { logger.error({ err: 'hello' }, 'test error') return res.send('error\n') }) @@ -208,7 +207,7 @@ if (Settings.processLifespanLimitMs) { function runSmokeTest() { if (Settings.processTooOld) return logger.log('running smoke tests') - smokeTest.triggerRun(err => { + smokeTest.triggerRun((err) => { if (err) logger.error({ err }, 'smoke tests failed') setTimeout(runSmokeTest, 30 * 1000) }) @@ -217,7 +216,7 @@ if (Settings.smokeTest) { runSmokeTest() } -app.get('/health_check', function(req, res) { +app.get('/health_check', function (req, res) { if (Settings.processTooOld) { return res.status(500).json({ processTooOld: true }) } @@ -226,7 +225,7 @@ app.get('/health_check', function(req, res) { app.get('/smoke_test_force', (req, res) => smokeTest.sendNewResult(res)) -app.use(function(error, req, res, next) { +app.use(function (error, req, res, next) { if (error instanceof Errors.NotFoundError) { logger.log({ err: error, url: req.url }, 'not found error') return res.sendStatus(404) @@ -244,8 +243,8 @@ const os = require('os') let STATE = 'up' -const loadTcpServer = net.createServer(function(socket) { - socket.on('error', function(err) { +const loadTcpServer = net.createServer(function (socket) { + socket.on('error', function (err) { if (err.code === 'ECONNRESET') { // this always comes up, we don't know why return @@ -280,19 +279,19 @@ const loadTcpServer = net.createServer(function(socket) { const loadHttpServer = express() -loadHttpServer.post('/state/up', function(req, res, next) { +loadHttpServer.post('/state/up', function (req, res, next) { STATE = 'up' logger.info('getting message to set server to down') return res.sendStatus(204) }) -loadHttpServer.post('/state/down', function(req, res, next) { +loadHttpServer.post('/state/down', function (req, res, next) { STATE = 'down' logger.info('getting message to set server to down') return res.sendStatus(204) }) -loadHttpServer.post('/state/maint', function(req, res, next) { +loadHttpServer.post('/state/maint', function (req, res, next) { STATE = 'maint' logger.info('getting message to set server to maint') return res.sendStatus(204) @@ -301,12 +300,12 @@ loadHttpServer.post('/state/maint', function(req, res, next) { const port = __guard__( Settings.internal != null ? Settings.internal.clsi : undefined, - x => x.port + (x) => x.port ) || 3013 const host = __guard__( Settings.internal != null ? Settings.internal.clsi : undefined, - x1 => x1.host + (x1) => x1.host ) || 'localhost' const loadTcpPort = Settings.internal.load_balancer_agent.load_port @@ -314,7 +313,7 @@ const loadHttpPort = Settings.internal.load_balancer_agent.local_port if (!module.parent) { // Called directly - app.listen(port, host, error => { + app.listen(port, host, (error) => { if (error) { logger.fatal({ error }, `Error starting CLSI on ${host}:${port}`) } else { @@ -322,14 +321,14 @@ if (!module.parent) { } }) - loadTcpServer.listen(loadTcpPort, host, function(error) { + loadTcpServer.listen(loadTcpPort, host, function (error) { if (error != null) { throw error } return logger.info(`Load tcp agent listening on load port ${loadTcpPort}`) }) - loadHttpServer.listen(loadHttpPort, host, function(error) { + loadHttpServer.listen(loadHttpPort, host, function (error) { if (error != null) { throw error } diff --git a/app/js/CompileController.js b/app/js/CompileController.js index 9c18830d..fb7367ce 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -24,10 +24,10 @@ const Errors = require('./Errors') module.exports = CompileController = { compile(req, res, next) { if (next == null) { - next = function(error) {} + next = function (error) {} } const timer = new Metrics.Timer('compile-request') - return RequestParser.parse(req.body, function(error, request) { + return RequestParser.parse(req.body, function (error, request) { if (error != null) { return next(error) } @@ -37,11 +37,11 @@ module.exports = CompileController = { } return ProjectPersistenceManager.markProjectAsJustAccessed( request.project_id, - function(error) { + function (error) { if (error != null) { return next(error) } - return CompileManager.doCompileWithLock(request, function( + return CompileManager.doCompileWithLock(request, function ( error, outputFiles ) { @@ -116,7 +116,7 @@ module.exports = CompileController = { compile: { status, error: (error != null ? error.message : undefined) || error, - outputFiles: outputFiles.map(file => ({ + outputFiles: outputFiles.map((file) => ({ url: `${Settings.apis.clsi.url}/project/${request.project_id}` + (request.user_id != null @@ -138,7 +138,7 @@ module.exports = CompileController = { stopCompile(req, res, next) { const { project_id, user_id } = req.params - return CompileManager.stopCompile(project_id, user_id, function(error) { + return CompileManager.stopCompile(project_id, user_id, function (error) { if (error != null) { return next(error) } @@ -148,12 +148,12 @@ module.exports = CompileController = { clearCache(req, res, next) { if (next == null) { - next = function(error) {} + next = function (error) {} } return ProjectPersistenceManager.clearProject( req.params.project_id, req.params.user_id, - function(error) { + function (error) { if (error != null) { return next(error) } @@ -164,7 +164,7 @@ module.exports = CompileController = { syncFromCode(req, res, next) { if (next == null) { - next = function(error) {} + next = function (error) {} } const { file } = req.query const line = parseInt(req.query.line, 10) @@ -177,7 +177,7 @@ module.exports = CompileController = { file, line, column, - function(error, pdfPositions) { + function (error, pdfPositions) { if (error != null) { return next(error) } @@ -190,29 +190,33 @@ module.exports = CompileController = { syncFromPdf(req, res, next) { if (next == null) { - next = function(error) {} + next = function (error) {} } const page = parseInt(req.query.page, 10) const h = parseFloat(req.query.h) const v = parseFloat(req.query.v) const { project_id } = req.params const { user_id } = req.params - return CompileManager.syncFromPdf(project_id, user_id, page, h, v, function( - error, - codePositions - ) { - if (error != null) { - return next(error) + return CompileManager.syncFromPdf( + project_id, + user_id, + page, + h, + v, + function (error, codePositions) { + if (error != null) { + return next(error) + } + return res.json({ + code: codePositions + }) } - return res.json({ - code: codePositions - }) - }) + ) }, wordcount(req, res, next) { if (next == null) { - next = function(error) {} + next = function (error) {} } const file = req.query.file || 'main.tex' const { project_id } = req.params @@ -229,7 +233,7 @@ module.exports = CompileController = { } logger.log({ image, file, project_id }, 'word count request') - return CompileManager.wordcount(project_id, user_id, file, image, function( + return CompileManager.wordcount(project_id, user_id, file, image, function ( error, result ) { @@ -244,7 +248,7 @@ module.exports = CompileController = { status(req, res, next) { if (next == null) { - next = function(error) {} + next = function (error) {} } return res.send('OK') } diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 73db6499..68edde49 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -35,7 +35,7 @@ const async = require('async') const Errors = require('./Errors') const CommandRunner = require('./CommandRunner') -const getCompileName = function(project_id, user_id) { +const getCompileName = function (project_id, user_id) { if (user_id != null) { return `${project_id}-${user_id}` } else { @@ -49,19 +49,19 @@ const getCompileDir = (project_id, user_id) => module.exports = CompileManager = { doCompileWithLock(request, callback) { if (callback == null) { - callback = function(error, outputFiles) {} + callback = function (error, outputFiles) {} } const compileDir = getCompileDir(request.project_id, request.user_id) const lockFile = Path.join(compileDir, '.project-lock') // use a .project-lock file in the compile directory to prevent // simultaneous compiles - return fse.ensureDir(compileDir, function(error) { + return fse.ensureDir(compileDir, function (error) { if (error != null) { return callback(error) } return LockManager.runWithLock( lockFile, - releaseLock => CompileManager.doCompile(request, releaseLock), + (releaseLock) => CompileManager.doCompile(request, releaseLock), callback ) }) @@ -69,7 +69,7 @@ module.exports = CompileManager = { doCompile(request, callback) { if (callback == null) { - callback = function(error, outputFiles) {} + callback = function (error, outputFiles) {} } const compileDir = getCompileDir(request.project_id, request.user_id) let timer = new Metrics.Timer('write-to-disk') @@ -77,7 +77,7 @@ module.exports = CompileManager = { { project_id: request.project_id, user_id: request.user_id }, 'syncing resources to disk' ) - return ResourceWriter.syncResourcesToDisk(request, compileDir, function( + return ResourceWriter.syncResourcesToDisk(request, compileDir, function ( error, resourceList ) { @@ -109,7 +109,7 @@ module.exports = CompileManager = { ) timer.done() - const injectDraftModeIfRequired = function(callback) { + const injectDraftModeIfRequired = function (callback) { if (request.draft) { return DraftModeManager.injectDraftMode( Path.join(compileDir, request.rootResourcePath), @@ -120,12 +120,12 @@ module.exports = CompileManager = { } } - const createTikzFileIfRequired = callback => + const createTikzFileIfRequired = (callback) => TikzManager.checkMainFile( compileDir, request.rootResourcePath, resourceList, - function(error, needsMainFile) { + function (error, needsMainFile) { if (error != null) { return callback(error) } @@ -165,7 +165,7 @@ module.exports = CompileManager = { // apply a series of file modifications/creations for draft mode and tikz return async.series( [injectDraftModeIfRequired, createTikzFileIfRequired], - function(error) { + function (error) { if (error != null) { return callback(error) } @@ -177,9 +177,9 @@ module.exports = CompileManager = { request.imageName != null ? request.imageName.match(/:(.*)/) : undefined, - x1 => x1[1] + (x1) => x1[1] ), - x => x.replace(/\./g, '-') + (x) => x.replace(/\./g, '-') ) || 'default' if (!request.project_id.match(/^[0-9a-f]{24}$/)) { tag = 'other' @@ -202,13 +202,11 @@ module.exports = CompileManager = { environment: env, compileGroup: request.compileGroup }, - function(error, output, stats, timings) { + function (error, output, stats, timings) { // request was for validation only let metric_key, metric_value if (request.check === 'validate') { - const result = (error != null - ? error.code - : undefined) + const result = (error != null ? error.code : undefined) ? 'fail' : 'pass' error = new Error('validation') @@ -231,7 +229,7 @@ module.exports = CompileManager = { OutputFileFinder.findOutputFiles( resourceList, compileDir, - function(err, outputFiles) { + function (err, outputFiles) { if (err != null) { return callback(err) } @@ -289,7 +287,7 @@ module.exports = CompileManager = { return OutputFileFinder.findOutputFiles( resourceList, compileDir, - function(error, outputFiles) { + function (error, outputFiles) { if (error != null) { return callback(error) } @@ -309,7 +307,7 @@ module.exports = CompileManager = { stopCompile(project_id, user_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const compileName = getCompileName(project_id, user_id) return LatexRunner.killLatex(compileName, callback) @@ -317,16 +315,16 @@ module.exports = CompileManager = { clearProject(project_id, user_id, _callback) { if (_callback == null) { - _callback = function(error) {} + _callback = function (error) {} } - const callback = function(error) { + const callback = function (error) { _callback(error) - return (_callback = function() {}) + return (_callback = function () {}) } const compileDir = getCompileDir(project_id, user_id) - return CompileManager._checkDirectory(compileDir, function(err, exists) { + return CompileManager._checkDirectory(compileDir, function (err, exists) { if (err != null) { return callback(err) } @@ -339,9 +337,9 @@ module.exports = CompileManager = { proc.on('error', callback) let stderr = '' - proc.stderr.setEncoding('utf8').on('data', chunk => (stderr += chunk)) + proc.stderr.setEncoding('utf8').on('data', (chunk) => (stderr += chunk)) - return proc.on('close', function(code) { + return proc.on('close', function (code) { if (code === 0) { return callback(null) } else { @@ -353,26 +351,26 @@ module.exports = CompileManager = { _findAllDirs(callback) { if (callback == null) { - callback = function(error, allDirs) {} + callback = function (error, allDirs) {} } const root = Settings.path.compilesDir - return fs.readdir(root, function(err, files) { + return fs.readdir(root, function (err, files) { if (err != null) { return callback(err) } - const allDirs = Array.from(files).map(file => Path.join(root, file)) + const allDirs = Array.from(files).map((file) => Path.join(root, file)) return callback(null, allDirs) }) }, clearExpiredProjects(max_cache_age_ms, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const now = Date.now() // action for each directory const expireIfNeeded = (checkDir, cb) => - fs.stat(checkDir, function(err, stats) { + fs.stat(checkDir, function (err, stats) { if (err != null) { return cb() } // ignore errors checking directory @@ -385,7 +383,7 @@ module.exports = CompileManager = { } }) // iterate over all project directories - return CompileManager._findAllDirs(function(error, allDirs) { + return CompileManager._findAllDirs(function (error, allDirs) { if (error != null) { return callback() } @@ -395,9 +393,9 @@ module.exports = CompileManager = { _checkDirectory(compileDir, callback) { if (callback == null) { - callback = function(error, exists) {} + callback = function (error, exists) {} } - return fs.lstat(compileDir, function(err, stats) { + return fs.lstat(compileDir, function (err, stats) { if ((err != null ? err.code : undefined) === 'ENOENT') { return callback(null, false) // directory does not exist } else if (err != null) { @@ -423,7 +421,7 @@ module.exports = CompileManager = { // might not match the file path on the host. The .synctex.gz file however, will be accessed // wherever it is on the host. if (callback == null) { - callback = function(error, pdfPositions) {} + callback = function (error, pdfPositions) {} } const compileName = getCompileName(project_id, user_id) const base_dir = Settings.path.synctexBaseDir(compileName) @@ -431,7 +429,7 @@ module.exports = CompileManager = { const compileDir = getCompileDir(project_id, user_id) const synctex_path = `${base_dir}/output.pdf` const command = ['code', synctex_path, file_path, line, column] - CompileManager._runSynctex(project_id, user_id, command, function( + CompileManager._runSynctex(project_id, user_id, command, function ( error, stdout ) { @@ -448,14 +446,14 @@ module.exports = CompileManager = { syncFromPdf(project_id, user_id, page, h, v, callback) { if (callback == null) { - callback = function(error, filePositions) {} + callback = function (error, filePositions) {} } const compileName = getCompileName(project_id, user_id) const compileDir = getCompileDir(project_id, user_id) const base_dir = Settings.path.synctexBaseDir(compileName) const synctex_path = `${base_dir}/output.pdf` const command = ['pdf', synctex_path, page, h, v] - CompileManager._runSynctex(project_id, user_id, command, function( + CompileManager._runSynctex(project_id, user_id, command, function ( error, stdout ) { @@ -475,17 +473,17 @@ module.exports = CompileManager = { _checkFileExists(dir, filename, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const file = Path.join(dir, filename) - return fs.stat(dir, function(error, stats) { + return fs.stat(dir, function (error, stats) { if ((error != null ? error.code : undefined) === 'ENOENT') { return callback(new Errors.NotFoundError('no output directory')) } if (error != null) { return callback(error) } - return fs.stat(file, function(error, stats) { + return fs.stat(file, function (error, stats) { if ((error != null ? error.code : undefined) === 'ENOENT') { return callback(new Errors.NotFoundError('no output file')) } @@ -502,7 +500,7 @@ module.exports = CompileManager = { _runSynctex(project_id, user_id, command, callback) { if (callback == null) { - callback = function(error, stdout) {} + callback = function (error, stdout) {} } const seconds = 1000 @@ -512,7 +510,7 @@ module.exports = CompileManager = { const timeout = 60 * 1000 // increased to allow for large projects const compileName = getCompileName(project_id, user_id) const compileGroup = 'synctex' - CompileManager._checkFileExists(directory, 'output.synctex.gz', error => { + CompileManager._checkFileExists(directory, 'output.synctex.gz', (error) => { if (error) { return callback(error) } @@ -526,7 +524,7 @@ module.exports = CompileManager = { timeout, {}, compileGroup, - function(error, output) { + function (error, output) { if (error != null) { logger.err( { err: error, command, project_id, user_id }, @@ -576,7 +574,7 @@ module.exports = CompileManager = { wordcount(project_id, user_id, file_name, image, callback) { if (callback == null) { - callback = function(error, pdfPositions) {} + callback = function (error, pdfPositions) {} } logger.log({ project_id, user_id, file_name, image }, 'running wordcount') const file_path = `$COMPILE_DIR/${file_name}` @@ -591,7 +589,7 @@ module.exports = CompileManager = { const timeout = 60 * 1000 const compileName = getCompileName(project_id, user_id) const compileGroup = 'wordcount' - return fse.ensureDir(compileDir, function(error) { + return fse.ensureDir(compileDir, function (error) { if (error != null) { logger.err( { error, project_id, user_id, file_name }, @@ -607,14 +605,14 @@ module.exports = CompileManager = { timeout, {}, compileGroup, - function(error) { + function (error) { if (error != null) { return callback(error) } return fs.readFile( compileDir + '/' + file_name + '.wc', 'utf-8', - function(err, stdout) { + function (err, stdout) { if (err != null) { // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored logger.err( diff --git a/app/js/DockerLockManager.js b/app/js/DockerLockManager.js index 2685b42d..0ed09856 100644 --- a/app/js/DockerLockManager.js +++ b/app/js/DockerLockManager.js @@ -23,7 +23,7 @@ module.exports = LockManager = { tryLock(key, callback) { let lockValue if (callback == null) { - callback = function(err, gotLock) {} + callback = function (err, gotLock) {} } const existingLock = LockState[key] if (existingLock != null) { @@ -46,11 +46,11 @@ module.exports = LockManager = { getLock(key, callback) { let attempt if (callback == null) { - callback = function(error, lockValue) {} + callback = function (error, lockValue) {} } const startTime = Date.now() return (attempt = () => - LockManager.tryLock(key, function(error, gotLock, lockValue) { + LockManager.tryLock(key, function (error, gotLock, lockValue) { if (error != null) { return callback(error) } @@ -68,7 +68,7 @@ module.exports = LockManager = { releaseLock(key, lockValue, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const existingLock = LockState[key] if (existingLock === lockValue) { @@ -93,14 +93,14 @@ module.exports = LockManager = { runWithLock(key, runner, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return LockManager.getLock(key, function(error, lockValue) { + return LockManager.getLock(key, function (error, lockValue) { if (error != null) { return callback(error) } return runner((error1, ...args) => - LockManager.releaseLock(key, lockValue, function(error2) { + LockManager.releaseLock(key, lockValue, function (error2) { error = error1 || error2 if (error != null) { return callback(error) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 49c7f402..72345392 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -32,7 +32,7 @@ logger.info('using docker runner') const usingSiblingContainers = () => __guard__( Settings != null ? Settings.path : undefined, - x => x.sandboxedCompilesHostDir + (x) => x.sandboxedCompilesHostDir ) != null let containerMonitorTimeout @@ -56,7 +56,7 @@ module.exports = DockerRunner = { ) { let name if (callback == null) { - callback = function(error, output) {} + callback = function (error, output) {} } if (usingSiblingContainers()) { const _newPath = Settings.path.sandboxedCompilesHostDir @@ -77,8 +77,8 @@ module.exports = DockerRunner = { const volumes = {} volumes[directory] = '/compile' - command = Array.from(command).map(arg => - __guardMethod__(arg.toString(), 'replace', o => + command = Array.from(command).map((arg) => + __guardMethod__(arg.toString(), 'replace', (o) => o.replace('$COMPILE_DIR', '/compile') ) ) @@ -112,7 +112,7 @@ module.exports = DockerRunner = { // logOptions = _.clone(options) // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" logger.log({ project_id }, 'running docker container') - DockerRunner._runAndWaitForContainer(options, volumes, timeout, function( + DockerRunner._runAndWaitForContainer(options, volumes, timeout, function ( error, output ) { @@ -121,7 +121,9 @@ module.exports = DockerRunner = { { err: error, project_id }, 'error running container so destroying and retrying' ) - return DockerRunner.destroyContainer(name, null, true, function(error) { + return DockerRunner.destroyContainer(name, null, true, function ( + error + ) { if (error != null) { return callback(error) } @@ -142,15 +144,17 @@ module.exports = DockerRunner = { kill(container_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } logger.log({ container_id }, 'sending kill signal to container') const container = dockerode.getContainer(container_id) - return container.kill(function(error) { + return container.kill(function (error) { if ( error != null && - __guardMethod__(error != null ? error.message : undefined, 'match', o => - o.match(/Cannot kill container .* is not running/) + __guardMethod__( + error != null ? error.message : undefined, + 'match', + (o) => o.match(/Cannot kill container .* is not running/) ) ) { logger.warn( @@ -170,12 +174,12 @@ module.exports = DockerRunner = { _runAndWaitForContainer(options, volumes, timeout, _callback) { if (_callback == null) { - _callback = function(error, output) {} + _callback = function (error, output) {} } - const callback = function(...args) { + const callback = function (...args) { _callback(...Array.from(args || [])) // Only call the callback once - return (_callback = function() {}) + return (_callback = function () {}) } const { name } = options @@ -184,13 +188,13 @@ module.exports = DockerRunner = { let containerReturned = false let output = {} - const callbackIfFinished = function() { + const callbackIfFinished = function () { if (streamEnded && containerReturned) { return callback(null, output) } } - const attachStreamHandler = function(error, _output) { + const attachStreamHandler = function (error, _output) { if (error != null) { return callback(error) } @@ -203,12 +207,12 @@ module.exports = DockerRunner = { options, volumes, attachStreamHandler, - function(error, containerId) { + function (error, containerId) { if (error != null) { return callback(error) } - return DockerRunner.waitForContainer(name, timeout, function( + return DockerRunner.waitForContainer(name, timeout, function ( error, exitCode ) { @@ -231,7 +235,7 @@ module.exports = DockerRunner = { containerReturned = true __guard__( options != null ? options.HostConfig : undefined, - x => (x.SecurityOpt = null) + (x) => (x.SecurityOpt = null) ) // small log line logger.log({ err, exitCode, options }, 'docker container has exited') return callbackIfFinished() @@ -357,21 +361,18 @@ module.exports = DockerRunner = { _fingerprintContainer(containerOptions) { // Yay, Hashing! const json = JSON.stringify(containerOptions) - return crypto - .createHash('md5') - .update(json) - .digest('hex') + return crypto.createHash('md5').update(json).digest('hex') }, startContainer(options, volumes, attachStreamHandler, callback) { return LockManager.runWithLock( options.name, - releaseLock => + (releaseLock) => // Check that volumes exist before starting the container. // When a container is started with volume pointing to a // non-existent directory then docker creates the directory but // with root ownership. - DockerRunner._checkVolumes(options, volumes, function(err) { + DockerRunner._checkVolumes(options, volumes, function (err) { if (err != null) { return releaseLock(err) } @@ -390,7 +391,7 @@ module.exports = DockerRunner = { // Check that volumes exist and are directories _checkVolumes(options, volumes, callback) { if (callback == null) { - callback = function(error, containerName) {} + callback = function (error, containerName) {} } if (usingSiblingContainers()) { // Server Pro, with sibling-containers active, skip checks @@ -398,7 +399,7 @@ module.exports = DockerRunner = { } const checkVolume = (path, cb) => - fs.stat(path, function(err, stats) { + fs.stat(path, function (err, stats) { if (err != null) { return cb(err) } @@ -409,14 +410,14 @@ module.exports = DockerRunner = { }) const jobs = [] for (const vol in volumes) { - ;(vol => jobs.push(cb => checkVolume(vol, cb)))(vol) + ;((vol) => jobs.push((cb) => checkVolume(vol, cb)))(vol) } return async.series(jobs, callback) }, _startContainer(options, volumes, attachStreamHandler, callback) { if (callback == null) { - callback = function(error, output) {} + callback = function (error, output) {} } callback = _.once(callback) const { name } = options @@ -425,7 +426,7 @@ module.exports = DockerRunner = { const container = dockerode.getContainer(name) const createAndStartContainer = () => - dockerode.createContainer(options, function(error, container) { + dockerode.createContainer(options, function (error, container) { if (error != null) { return callback(error) } @@ -435,11 +436,11 @@ module.exports = DockerRunner = { DockerRunner.attachToContainer( options.name, attachStreamHandler, - function(error) { + function (error) { if (error != null) { return callback(error) } - return container.start(function(error) { + return container.start(function (error) { if ( error != null && (error != null ? error.statusCode : undefined) !== 304 @@ -452,7 +453,7 @@ module.exports = DockerRunner = { }) } ) - return container.inspect(function(error, stats) { + return container.inspect(function (error, stats) { if ((error != null ? error.statusCode : undefined) === 404) { return createAndStartContainer() } else if (error != null) { @@ -469,7 +470,7 @@ module.exports = DockerRunner = { attachToContainer(containerId, attachStreamHandler, attachStartCallback) { const container = dockerode.getContainer(containerId) - return container.attach({ stdout: 1, stderr: 1, stream: 1 }, function( + return container.attach({ stdout: 1, stderr: 1, stream: 1 }, function ( error, stream ) { @@ -486,7 +487,7 @@ module.exports = DockerRunner = { logger.log({ container_id: containerId }, 'attached to container') const MAX_OUTPUT = 1024 * 1024 // limit output to 1MB - const createStringOutputStream = function(name) { + const createStringOutputStream = function (name) { return { data: '', overflowed: false, @@ -519,7 +520,7 @@ module.exports = DockerRunner = { container.modem.demuxStream(stream, stdout, stderr) - stream.on('error', err => + stream.on('error', (err) => logger.error( { err, container_id: containerId }, 'error reading from container stream' @@ -534,28 +535,28 @@ module.exports = DockerRunner = { waitForContainer(containerId, timeout, _callback) { if (_callback == null) { - _callback = function(error, exitCode) {} + _callback = function (error, exitCode) {} } - const callback = function(...args) { + const callback = function (...args) { _callback(...Array.from(args || [])) // Only call the callback once - return (_callback = function() {}) + return (_callback = function () {}) } const container = dockerode.getContainer(containerId) let timedOut = false - const timeoutId = setTimeout(function() { + const timeoutId = setTimeout(function () { timedOut = true logger.log( { container_id: containerId }, 'timeout reached, killing container' ) - return container.kill(function() {}) + return container.kill(function () {}) }, timeout) logger.log({ container_id: containerId }, 'waiting for docker container') - return container.wait(function(error, res) { + return container.wait(function (error, res) { if (error != null) { clearTimeout(timeoutId) logger.error( @@ -588,11 +589,11 @@ module.exports = DockerRunner = { // error callback. We fall back to deleting by name if no id is // supplied. if (callback == null) { - callback = function(error) {} + callback = function (error) {} } return LockManager.runWithLock( containerName, - releaseLock => + (releaseLock) => DockerRunner._destroyContainer( containerId || containerName, shouldForce, @@ -604,11 +605,11 @@ module.exports = DockerRunner = { _destroyContainer(containerId, shouldForce, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } logger.log({ container_id: containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) - return container.remove({ force: shouldForce === true }, function(error) { + return container.remove({ force: shouldForce === true }, function (error) { if ( error != null && (error != null ? error.statusCode : undefined) === 404 @@ -638,7 +639,7 @@ module.exports = DockerRunner = { examineOldContainer(container, callback) { if (callback == null) { - callback = function(error, name, id, ttl) {} + callback = function (error, name, id, ttl) {} } const name = container.Name || @@ -657,16 +658,19 @@ module.exports = DockerRunner = { destroyOldContainers(callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return dockerode.listContainers({ all: true }, function(error, containers) { + return dockerode.listContainers({ all: true }, function ( + error, + containers + ) { if (error != null) { return callback(error) } const jobs = [] for (const container of Array.from(containers || [])) { - ;(container => - DockerRunner.examineOldContainer(container, function( + ;((container) => + DockerRunner.examineOldContainer(container, function ( err, name, id, @@ -676,7 +680,7 @@ module.exports = DockerRunner = { // strip the / prefix // the LockManager uses the plain container name name = name.slice(1) - return jobs.push(cb => + return jobs.push((cb) => DockerRunner.destroyContainer(name, id, false, () => cb()) ) } diff --git a/app/js/DraftModeManager.js b/app/js/DraftModeManager.js index c8f59aa6..0bdd40f0 100644 --- a/app/js/DraftModeManager.js +++ b/app/js/DraftModeManager.js @@ -18,9 +18,9 @@ const logger = require('logger-sharelatex') module.exports = DraftModeManager = { injectDraftMode(filename, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return fs.readFile(filename, 'utf8', function(error, content) { + return fs.readFile(filename, 'utf8', function (error, content) { if (error != null) { return callback(error) } diff --git a/app/js/Errors.js b/app/js/Errors.js index d3a5f5a0..9b014f8b 100644 --- a/app/js/Errors.js +++ b/app/js/Errors.js @@ -5,7 +5,7 @@ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. let Errors -var NotFoundError = function(message) { +var NotFoundError = function (message) { const error = new Error(message) error.name = 'NotFoundError' error.__proto__ = NotFoundError.prototype @@ -13,7 +13,7 @@ var NotFoundError = function(message) { } NotFoundError.prototype.__proto__ = Error.prototype -var FilesOutOfSyncError = function(message) { +var FilesOutOfSyncError = function (message) { const error = new Error(message) error.name = 'FilesOutOfSyncError' error.__proto__ = FilesOutOfSyncError.prototype @@ -21,7 +21,7 @@ var FilesOutOfSyncError = function(message) { } FilesOutOfSyncError.prototype.__proto__ = Error.prototype -var AlreadyCompilingError = function(message) { +var AlreadyCompilingError = function (message) { const error = new Error(message) error.name = 'AlreadyCompilingError' error.__proto__ = AlreadyCompilingError.prototype diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index 6d1591a2..f8799d21 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -27,7 +27,7 @@ module.exports = LatexRunner = { runLatex(project_id, options, callback) { let command if (callback == null) { - callback = function(error) {} + callback = function (error) {} } let { directory, @@ -89,20 +89,20 @@ module.exports = LatexRunner = { timeout, environment, compileGroup, - function(error, output) { + function (error, output) { delete ProcessTable[id] if (error != null) { return callback(error) } const runs = __guard__( - __guard__(output != null ? output.stderr : undefined, x1 => + __guard__(output != null ? output.stderr : undefined, (x1) => x1.match(/^Run number \d+ of .*latex/gm) ), - x => x.length + (x) => x.length ) || 0 const failed = - __guard__(output != null ? output.stdout : undefined, x2 => + __guard__(output != null ? output.stdout : undefined, (x2) => x2.match(/^Latexmk: Errors/m) ) != null ? 1 @@ -122,21 +122,21 @@ module.exports = LatexRunner = { stderr != null ? stderr.match(/Percent of CPU this job got: (\d+)/m) : undefined, - x3 => x3[1] + (x3) => x3[1] ) || 0 timings['cpu-time'] = __guard__( stderr != null ? stderr.match(/User time.*: (\d+.\d+)/m) : undefined, - x4 => x4[1] + (x4) => x4[1] ) || 0 timings['sys-time'] = __guard__( stderr != null ? stderr.match(/System time.*: (\d+.\d+)/m) : undefined, - x5 => x5[1] + (x5) => x5[1] ) || 0 // record output files LatexRunner.writeLogOutput(project_id, directory, output, () => { @@ -153,7 +153,7 @@ module.exports = LatexRunner = { // internal method for writing non-empty log files function _writeFile(file, content, cb) { if (content && content.length > 0) { - fs.writeFile(file, content, err => { + fs.writeFile(file, content, (err) => { if (err) { logger.error({ project_id, file }, 'error writing log file') // don't fail on error } @@ -173,7 +173,7 @@ module.exports = LatexRunner = { killLatex(project_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const id = `${project_id}` logger.log({ id }, 'killing running compile') @@ -202,7 +202,7 @@ module.exports = LatexRunner = { return ( __guard__( Settings != null ? Settings.clsi : undefined, - x => x.latexmkCommandPrefix + (x) => x.latexmkCommandPrefix ) || [] ).concat(args) }, diff --git a/app/js/LocalCommandRunner.js b/app/js/LocalCommandRunner.js index f16fa16b..6f57731c 100644 --- a/app/js/LocalCommandRunner.js +++ b/app/js/LocalCommandRunner.js @@ -33,11 +33,11 @@ module.exports = CommandRunner = { ) { let key, value if (callback == null) { - callback = function(error) {} + callback = function (error) {} } else { callback = _.once(callback) } - command = Array.from(command).map(arg => + command = Array.from(command).map((arg) => arg.toString().replace('$COMPILE_DIR', directory) ) logger.log({ project_id, command, directory }, 'running command') @@ -58,9 +58,9 @@ module.exports = CommandRunner = { const proc = spawn(command[0], command.slice(1), { cwd: directory, env }) let stdout = '' - proc.stdout.setEncoding('utf8').on('data', data => (stdout += data)) + proc.stdout.setEncoding('utf8').on('data', (data) => (stdout += data)) - proc.on('error', function(err) { + proc.on('error', function (err) { logger.err( { err, project_id, command, directory }, 'error running command' @@ -68,7 +68,7 @@ module.exports = CommandRunner = { return callback(err) }) - proc.on('close', function(code, signal) { + proc.on('close', function (code, signal) { let err logger.info({ code, signal, project_id }, 'command exited') if (signal === 'SIGTERM') { @@ -91,7 +91,7 @@ module.exports = CommandRunner = { kill(pid, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } try { process.kill(-pid) // kill all processes in group diff --git a/app/js/LockManager.js b/app/js/LockManager.js index 2da7da10..1246cc98 100644 --- a/app/js/LockManager.js +++ b/app/js/LockManager.js @@ -25,20 +25,20 @@ module.exports = LockManager = { runWithLock(path, runner, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const lockOpts = { wait: this.MAX_LOCK_WAIT_TIME, pollPeriod: this.LOCK_TEST_INTERVAL, stale: this.LOCK_STALE } - return Lockfile.lock(path, lockOpts, function(error) { + return Lockfile.lock(path, lockOpts, function (error) { if ((error != null ? error.code : undefined) === 'EEXIST') { return callback(new Errors.AlreadyCompilingError('compile in progress')) } else if (error != null) { return fs.lstat(path, (statLockErr, statLock) => fs.lstat(Path.dirname(path), (statDirErr, statDir) => - fs.readdir(Path.dirname(path), function(readdirErr, readdirDir) { + fs.readdir(Path.dirname(path), function (readdirErr, readdirDir) { logger.err( { error, @@ -58,7 +58,7 @@ module.exports = LockManager = { ) } else { return runner((error1, ...args) => - Lockfile.unlock(path, function(error2) { + Lockfile.unlock(path, function (error2) { error = error1 || error2 if (error != null) { return callback(error) diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index c0b0d6ed..b34dea79 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -47,9 +47,9 @@ module.exports = OutputCacheManager = { generateBuildId(callback) { // generate a secure build id from Date.now() and 8 random bytes in hex if (callback == null) { - callback = function(error, buildId) {} + callback = function (error, buildId) {} } - return crypto.randomBytes(8, function(err, buf) { + return crypto.randomBytes(8, function (err, buf) { if (err != null) { return callback(err) } @@ -61,9 +61,9 @@ module.exports = OutputCacheManager = { saveOutputFiles(outputFiles, compileDir, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return OutputCacheManager.generateBuildId(function(err, buildId) { + return OutputCacheManager.generateBuildId(function (err, buildId) { if (err != null) { return callback(err) } @@ -80,7 +80,7 @@ module.exports = OutputCacheManager = { // make a compileDir/CACHE_SUBDIR/build_id directory and // copy all the output files into it if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR) // Put the files into a new cache subdirectory @@ -99,17 +99,20 @@ module.exports = OutputCacheManager = { (Settings.clsi != null ? Settings.clsi.archive_logs : undefined) || (Settings.clsi != null ? Settings.clsi.strace : undefined) ) { - OutputCacheManager.archiveLogs(outputFiles, compileDir, buildId, function( - err - ) { - if (err != null) { - return logger.warn({ err }, 'erroring archiving log files') + OutputCacheManager.archiveLogs( + outputFiles, + compileDir, + buildId, + function (err) { + if (err != null) { + return logger.warn({ err }, 'erroring archiving log files') + } } - }) + ) } // make the new cache directory - return fse.ensureDir(cacheDir, function(err) { + return fse.ensureDir(cacheDir, function (err) { if (err != null) { logger.error( { err, directory: cacheDir }, @@ -121,7 +124,7 @@ module.exports = OutputCacheManager = { const results = [] return async.mapSeries( outputFiles, - function(file, cb) { + function (file, cb) { // don't send dot files as output, express doesn't serve them if (OutputCacheManager._fileIsHidden(file.path)) { logger.debug( @@ -136,7 +139,7 @@ module.exports = OutputCacheManager = { Path.join(compileDir, file.path), Path.join(cacheDir, file.path) ]) - return OutputCacheManager._checkFileIsSafe(src, function( + return OutputCacheManager._checkFileIsSafe(src, function ( err, isSafe ) { @@ -146,7 +149,7 @@ module.exports = OutputCacheManager = { if (!isSafe) { return cb() } - return OutputCacheManager._checkIfShouldCopy(src, function( + return OutputCacheManager._checkIfShouldCopy(src, function ( err, shouldCopy ) { @@ -156,7 +159,7 @@ module.exports = OutputCacheManager = { if (!shouldCopy) { return cb() } - return OutputCacheManager._copyFile(src, dst, function(err) { + return OutputCacheManager._copyFile(src, dst, function (err) { if (err != null) { return cb(err) } @@ -167,12 +170,12 @@ module.exports = OutputCacheManager = { }) }) }, - function(err) { + function (err) { if (err != null) { // pass back the original files if we encountered *any* error callback(err, outputFiles) // clean up the directory we just created - return fse.remove(cacheDir, function(err) { + return fse.remove(cacheDir, function (err) { if (err != null) { return logger.error( { err, dir: cacheDir }, @@ -197,7 +200,7 @@ module.exports = OutputCacheManager = { archiveLogs(outputFiles, compileDir, buildId, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const archiveDir = Path.join( compileDir, @@ -205,18 +208,18 @@ module.exports = OutputCacheManager = { buildId ) logger.log({ dir: archiveDir }, 'archiving log files for project') - return fse.ensureDir(archiveDir, function(err) { + return fse.ensureDir(archiveDir, function (err) { if (err != null) { return callback(err) } return async.mapSeries( outputFiles, - function(file, cb) { + function (file, cb) { const [src, dst] = Array.from([ Path.join(compileDir, file.path), Path.join(archiveDir, file.path) ]) - return OutputCacheManager._checkFileIsSafe(src, function( + return OutputCacheManager._checkFileIsSafe(src, function ( err, isSafe ) { @@ -226,7 +229,7 @@ module.exports = OutputCacheManager = { if (!isSafe) { return cb() } - return OutputCacheManager._checkIfShouldArchive(src, function( + return OutputCacheManager._checkIfShouldArchive(src, function ( err, shouldArchive ) { @@ -248,9 +251,9 @@ module.exports = OutputCacheManager = { expireOutputFiles(cacheRoot, options, callback) { // look in compileDir for build dirs and delete if > N or age of mod time > T if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return fs.readdir(cacheRoot, function(err, results) { + return fs.readdir(cacheRoot, function (err, results) { if (err != null) { if (err.code === 'ENOENT') { return callback(null) @@ -262,7 +265,7 @@ module.exports = OutputCacheManager = { const dirs = results.sort().reverse() const currentTime = Date.now() - const isExpired = function(dir, index) { + const isExpired = function (dir, index) { if ((options != null ? options.keep : undefined) === dir) { return false } @@ -280,7 +283,7 @@ module.exports = OutputCacheManager = { // we can get the build time from the first part of the directory name DDDD-RRRR // DDDD is date and RRRR is random bytes const dirTime = parseInt( - __guard__(dir.split('-'), x => x[0]), + __guard__(dir.split('-'), (x) => x[0]), 16 ) const age = currentTime - dirTime @@ -290,7 +293,7 @@ module.exports = OutputCacheManager = { const toRemove = _.filter(dirs, isExpired) const removeDir = (dir, cb) => - fse.remove(Path.join(cacheRoot, dir), function(err, result) { + fse.remove(Path.join(cacheRoot, dir), function (err, result) { logger.log({ cache: cacheRoot, dir }, 'removed expired cache dir') if (err != null) { logger.error({ err, dir }, 'cache remove error') @@ -312,9 +315,9 @@ module.exports = OutputCacheManager = { _checkFileIsSafe(src, callback) { // check if we have a valid file to copy into the cache if (callback == null) { - callback = function(error, isSafe) {} + callback = function (error, isSafe) {} } - return fs.stat(src, function(err, stats) { + return fs.stat(src, function (err, stats) { if ((err != null ? err.code : undefined) === 'ENOENT') { logger.warn( { err, file: src }, @@ -341,7 +344,7 @@ module.exports = OutputCacheManager = { _copyFile(src, dst, callback) { // copy output file into the cache - return fse.copy(src, dst, function(err) { + return fse.copy(src, dst, function (err) { if ((err != null ? err.code : undefined) === 'ENOENT') { logger.warn( { err, file: src }, @@ -368,7 +371,7 @@ module.exports = OutputCacheManager = { _checkIfShouldCopy(src, callback) { if (callback == null) { - callback = function(err, shouldCopy) {} + callback = function (err, shouldCopy) {} } return callback(null, !Path.basename(src).match(/^strace/)) }, @@ -376,7 +379,7 @@ module.exports = OutputCacheManager = { _checkIfShouldArchive(src, callback) { let needle if (callback == null) { - callback = function(err, shouldCopy) {} + callback = function (err, shouldCopy) {} } if (Path.basename(src).match(/^strace/)) { return callback(null, true) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 83199781..f863e0c1 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -24,14 +24,14 @@ const logger = require('logger-sharelatex') module.exports = OutputFileFinder = { findOutputFiles(resources, directory, callback) { if (callback == null) { - callback = function(error, outputFiles, allFiles) {} + callback = function (error, outputFiles, allFiles) {} } const incomingResources = {} for (const resource of Array.from(resources)) { incomingResources[resource.path] = true } - return OutputFileFinder._getAllFiles(directory, function(error, allFiles) { + return OutputFileFinder._getAllFiles(directory, function (error, allFiles) { if (allFiles == null) { allFiles = [] } @@ -44,7 +44,7 @@ module.exports = OutputFileFinder = { if (!incomingResources[file]) { outputFiles.push({ path: file, - type: __guard__(file.match(/\.([^\.]+)$/), x => x[1]) + type: __guard__(file.match(/\.([^\.]+)$/), (x) => x[1]) }) } } @@ -54,11 +54,11 @@ module.exports = OutputFileFinder = { _getAllFiles(directory, _callback) { if (_callback == null) { - _callback = function(error, fileList) {} + _callback = function (error, fileList) {} } - const callback = function(error, fileList) { + const callback = function (error, fileList) { _callback(error, fileList) - return (_callback = function() {}) + return (_callback = function () {}) } // don't include clsi-specific files/directories in the output list @@ -87,9 +87,9 @@ module.exports = OutputFileFinder = { const proc = spawn('find', args) let stdout = '' - proc.stdout.setEncoding('utf8').on('data', chunk => (stdout += chunk)) + proc.stdout.setEncoding('utf8').on('data', (chunk) => (stdout += chunk)) proc.on('error', callback) - return proc.on('close', function(code) { + return proc.on('close', function (code) { if (code !== 0) { logger.warn( { directory, code }, @@ -98,7 +98,7 @@ module.exports = OutputFileFinder = { return callback(null, []) } let fileList = stdout.trim().split('\n') - fileList = fileList.map(function(file) { + fileList = fileList.map(function (file) { // Strip leading directory let path return (path = Path.relative(directory, file)) diff --git a/app/js/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js index e3b3e60e..85273762 100644 --- a/app/js/OutputFileOptimiser.js +++ b/app/js/OutputFileOptimiser.js @@ -26,10 +26,10 @@ module.exports = OutputFileOptimiser = { // check output file (src) and see if we can optimise it, storing // the result in the build directory (dst) if (callback == null) { - callback = function(error) {} + callback = function (error) {} } if (src.match(/\/output\.pdf$/)) { - return OutputFileOptimiser.checkIfPDFIsOptimised(src, function( + return OutputFileOptimiser.checkIfPDFIsOptimised(src, function ( err, isOptimised ) { @@ -46,12 +46,12 @@ module.exports = OutputFileOptimiser = { checkIfPDFIsOptimised(file, callback) { const SIZE = 16 * 1024 // check the header of the pdf const result = Buffer.alloc(SIZE) // fills with zeroes by default - return fs.open(file, 'r', function(err, fd) { + return fs.open(file, 'r', function (err, fd) { if (err != null) { return callback(err) } return fs.read(fd, result, 0, SIZE, 0, (errRead, bytesRead, buffer) => - fs.close(fd, function(errClose) { + fs.close(fd, function (errClose) { if (errRead != null) { return callback(errRead) } @@ -68,7 +68,7 @@ module.exports = OutputFileOptimiser = { optimisePDF(src, dst, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const tmpOutput = dst + '.opt' const args = ['--linearize', src, tmpOutput] @@ -77,19 +77,19 @@ module.exports = OutputFileOptimiser = { const timer = new Metrics.Timer('qpdf') const proc = spawn('qpdf', args) let stdout = '' - proc.stdout.setEncoding('utf8').on('data', chunk => (stdout += chunk)) + proc.stdout.setEncoding('utf8').on('data', (chunk) => (stdout += chunk)) callback = _.once(callback) // avoid double call back for error and close event - proc.on('error', function(err) { + proc.on('error', function (err) { logger.warn({ err, args }, 'qpdf failed') return callback(null) }) // ignore the error - return proc.on('close', function(code) { + return proc.on('close', function (code) { timer.done() if (code !== 0) { logger.warn({ code, args }, 'qpdf returned error') return callback(null) // ignore the error } - return fs.rename(tmpOutput, dst, function(err) { + return fs.rename(tmpOutput, dst, function (err) { if (err != null) { logger.warn( { tmpOutput, dst }, diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index 2c89f13f..536630a8 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -27,9 +27,9 @@ module.exports = ProjectPersistenceManager = { refreshExpiryTimeout(callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - diskusage.check('/', function(err, stats) { + diskusage.check('/', function (err, stats) { if (err) { logger.err({ err: err }, 'error getting disk usage') return callback(err) @@ -48,9 +48,9 @@ module.exports = ProjectPersistenceManager = { }, markProjectAsJustAccessed(project_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - const job = cb => + const job = (cb) => db.Project.findOrCreate({ where: { project_id } }) .spread((project, created) => project @@ -64,9 +64,9 @@ module.exports = ProjectPersistenceManager = { clearExpiredProjects(callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return ProjectPersistenceManager._findExpiredProjectIds(function( + return ProjectPersistenceManager._findExpiredProjectIds(function ( error, project_ids ) { @@ -74,9 +74,9 @@ module.exports = ProjectPersistenceManager = { return callback(error) } logger.log({ project_ids }, 'clearing expired projects') - const jobs = Array.from(project_ids || []).map(project_id => - (project_id => callback => - ProjectPersistenceManager.clearProjectFromCache(project_id, function( + const jobs = Array.from(project_ids || []).map((project_id) => + ((project_id) => (callback) => + ProjectPersistenceManager.clearProjectFromCache(project_id, function ( err ) { if (err != null) { @@ -85,13 +85,13 @@ module.exports = ProjectPersistenceManager = { return callback() }))(project_id) ) - return async.series(jobs, function(error) { + return async.series(jobs, function (error) { if (error != null) { return callback(error) } return CompileManager.clearExpiredProjects( ProjectPersistenceManager.EXPIRY_TIMEOUT, - error => callback() + (error) => callback() ) }) }) @@ -99,16 +99,16 @@ module.exports = ProjectPersistenceManager = { clearProject(project_id, user_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } logger.log({ project_id, user_id }, 'clearing project for user') - return CompileManager.clearProject(project_id, user_id, function(error) { + return CompileManager.clearProject(project_id, user_id, function (error) { if (error != null) { return callback(error) } return ProjectPersistenceManager.clearProjectFromCache( project_id, - function(error) { + function (error) { if (error != null) { return callback(error) } @@ -120,17 +120,17 @@ module.exports = ProjectPersistenceManager = { clearProjectFromCache(project_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } logger.log({ project_id }, 'clearing project from cache') - return UrlCache.clearProject(project_id, function(error) { + return UrlCache.clearProject(project_id, function (error) { if (error != null) { logger.err({ error, project_id }, 'error clearing project from cache') return callback(error) } return ProjectPersistenceManager._clearProjectFromDatabase( project_id, - function(error) { + function (error) { if (error != null) { logger.err( { error, project_id }, @@ -145,10 +145,10 @@ module.exports = ProjectPersistenceManager = { _clearProjectFromDatabase(project_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } logger.log({ project_id }, 'clearing project from database') - const job = cb => + const job = (cb) => db.Project.destroy({ where: { project_id } }) .then(() => cb()) .error(cb) @@ -157,19 +157,19 @@ module.exports = ProjectPersistenceManager = { _findExpiredProjectIds(callback) { if (callback == null) { - callback = function(error, project_ids) {} + callback = function (error, project_ids) {} } - const job = function(cb) { + const job = function (cb) { const keepProjectsFrom = new Date( Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT ) const q = {} q[db.op.lt] = keepProjectsFrom return db.Project.findAll({ where: { lastAccessed: q } }) - .then(projects => + .then((projects) => cb( null, - projects.map(project => project.project_id) + projects.map((project) => project.project_id) ) ) .error(cb) diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index 342dbc86..43772044 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -27,7 +27,7 @@ module.exports = RequestParser = { parse(body, callback) { let resource if (callback == null) { - callback = function(error, data) {} + callback = function (error, data) {} } const response = {} diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 5a5d811c..07cc7857 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -41,13 +41,13 @@ module.exports = ResourceStateManager = { saveProjectState(state, resources, basePath, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) if (state == null) { // remove the file if no state passed in logger.log({ state, basePath }, 'clearing sync state') - return fs.unlink(stateFile, function(err) { + return fs.unlink(stateFile, function (err) { if (err != null && err.code !== 'ENOENT') { return callback(err) } else { @@ -56,7 +56,9 @@ module.exports = ResourceStateManager = { }) } else { logger.log({ state, basePath }, 'writing sync state') - const resourceList = Array.from(resources).map(resource => resource.path) + const resourceList = Array.from(resources).map( + (resource) => resource.path + ) return fs.writeFile( stateFile, [...Array.from(resourceList), `stateHash:${state}`].join('\n'), @@ -67,11 +69,11 @@ module.exports = ResourceStateManager = { checkProjectStateMatches(state, basePath, callback) { if (callback == null) { - callback = function(error, resources) {} + callback = function (error, resources) {} } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) const size = this.SYNC_STATE_MAX_SIZE - return SafeReader.readFile(stateFile, size, 'utf8', function( + return SafeReader.readFile(stateFile, size, 'utf8', function ( err, result, bytesRead @@ -86,7 +88,7 @@ module.exports = ResourceStateManager = { ) } const array = - __guard__(result != null ? result.toString() : undefined, x => + __guard__(result != null ? result.toString() : undefined, (x) => x.split('\n') ) || [] const adjustedLength = Math.max(array.length, 1) @@ -102,7 +104,7 @@ module.exports = ResourceStateManager = { new Errors.FilesOutOfSyncError('invalid state for incremental update') ) } else { - const resources = Array.from(resourceList).map(path => ({ path })) + const resources = Array.from(resourceList).map((path) => ({ path })) return callback(null, resources) } }) @@ -112,11 +114,11 @@ module.exports = ResourceStateManager = { // check the paths are all relative to current directory let file if (callback == null) { - callback = function(error) {} + callback = function (error) {} } for (file of Array.from(resources || [])) { for (const dir of Array.from( - __guard__(file != null ? file.path : undefined, x => x.split('/')) + __guard__(file != null ? file.path : undefined, (x) => x.split('/')) )) { if (dir === '..') { return callback(new Error('relative path in resource file list')) @@ -129,8 +131,8 @@ module.exports = ResourceStateManager = { seenFile[file] = true } const missingFiles = Array.from(resources) - .filter(resource => !seenFile[resource.path]) - .map(resource => resource.path) + .filter((resource) => !seenFile[resource.path]) + .map((resource) => resource.path) if ((missingFiles != null ? missingFiles.length : undefined) > 0) { logger.err( { missingFiles, basePath, allFiles, resources }, diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index 97e971e1..1625ee15 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -30,7 +30,7 @@ const parallelFileDownloads = settings.parallelFileDownloads || 1 module.exports = ResourceWriter = { syncResourcesToDisk(request, basePath, callback) { if (callback == null) { - callback = function(error, resourceList) {} + callback = function (error, resourceList) {} } if (request.syncType === 'incremental') { logger.log( @@ -40,14 +40,14 @@ module.exports = ResourceWriter = { return ResourceStateManager.checkProjectStateMatches( request.syncState, basePath, - function(error, resourceList) { + function (error, resourceList) { if (error != null) { return callback(error) } return ResourceWriter._removeExtraneousFiles( resourceList, basePath, - function(error, outputFiles, allFiles) { + function (error, outputFiles, allFiles) { if (error != null) { return callback(error) } @@ -55,7 +55,7 @@ module.exports = ResourceWriter = { resourceList, allFiles, basePath, - function(error) { + function (error) { if (error != null) { return callback(error) } @@ -63,7 +63,7 @@ module.exports = ResourceWriter = { request.project_id, request.resources, basePath, - function(error) { + function (error) { if (error != null) { return callback(error) } @@ -85,7 +85,7 @@ module.exports = ResourceWriter = { request.project_id, request.resources, basePath, - function(error) { + function (error) { if (error != null) { return callback(error) } @@ -93,7 +93,7 @@ module.exports = ResourceWriter = { request.syncState, request.resources, basePath, - function(error) { + function (error) { if (error != null) { return callback(error) } @@ -107,15 +107,15 @@ module.exports = ResourceWriter = { saveIncrementalResourcesToDisk(project_id, resources, basePath, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return this._createDirectory(basePath, error => { + return this._createDirectory(basePath, (error) => { if (error != null) { return callback(error) } - const jobs = Array.from(resources).map(resource => - (resource => { - return callback => + const jobs = Array.from(resources).map((resource) => + ((resource) => { + return (callback) => this._writeResourceToDisk(project_id, resource, basePath, callback) })(resource) ) @@ -125,19 +125,19 @@ module.exports = ResourceWriter = { saveAllResourcesToDisk(project_id, resources, basePath, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return this._createDirectory(basePath, error => { + return this._createDirectory(basePath, (error) => { if (error != null) { return callback(error) } - return this._removeExtraneousFiles(resources, basePath, error => { + return this._removeExtraneousFiles(resources, basePath, (error) => { if (error != null) { return callback(error) } - const jobs = Array.from(resources).map(resource => - (resource => { - return callback => + const jobs = Array.from(resources).map((resource) => + ((resource) => { + return (callback) => this._writeResourceToDisk( project_id, resource, @@ -153,9 +153,9 @@ module.exports = ResourceWriter = { _createDirectory(basePath, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return fs.mkdir(basePath, function(err) { + return fs.mkdir(basePath, function (err) { if (err != null) { if (err.code === 'EEXIST') { return callback() @@ -171,15 +171,15 @@ module.exports = ResourceWriter = { _removeExtraneousFiles(resources, basePath, _callback) { if (_callback == null) { - _callback = function(error, outputFiles, allFiles) {} + _callback = function (error, outputFiles, allFiles) {} } const timer = new Metrics.Timer('unlink-output-files') - const callback = function(error, ...result) { + const callback = function (error, ...result) { timer.done() return _callback(error, ...Array.from(result)) } - return OutputFileFinder.findOutputFiles(resources, basePath, function( + return OutputFileFinder.findOutputFiles(resources, basePath, function ( error, outputFiles, allFiles @@ -190,7 +190,7 @@ module.exports = ResourceWriter = { const jobs = [] for (const file of Array.from(outputFiles || [])) { - ;(function(file) { + ;(function (file) { const { path } = file let should_delete = true if ( @@ -242,7 +242,7 @@ module.exports = ResourceWriter = { should_delete = true } if (should_delete) { - return jobs.push(callback => + return jobs.push((callback) => ResourceWriter._deleteFileIfNotDirectory( Path.join(basePath, path), callback @@ -252,7 +252,7 @@ module.exports = ResourceWriter = { })(file) } - return async.series(jobs, function(error) { + return async.series(jobs, function (error) { if (error != null) { return callback(error) } @@ -263,9 +263,9 @@ module.exports = ResourceWriter = { _deleteFileIfNotDirectory(path, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return fs.stat(path, function(error, stat) { + return fs.stat(path, function (error, stat) { if (error != null && error.code === 'ENOENT') { return callback() } else if (error != null) { @@ -275,7 +275,7 @@ module.exports = ResourceWriter = { ) return callback(error) } else if (stat.isFile()) { - return fs.unlink(path, function(error) { + return fs.unlink(path, function (error) { if (error != null) { logger.err( { err: error, path }, @@ -294,16 +294,18 @@ module.exports = ResourceWriter = { _writeResourceToDisk(project_id, resource, basePath, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return ResourceWriter.checkPath(basePath, resource.path, function( + return ResourceWriter.checkPath(basePath, resource.path, function ( error, path ) { if (error != null) { return callback(error) } - return fs.mkdir(Path.dirname(path), { recursive: true }, function(error) { + return fs.mkdir(Path.dirname(path), { recursive: true }, function ( + error + ) { if (error != null) { return callback(error) } @@ -314,7 +316,7 @@ module.exports = ResourceWriter = { resource.url, path, resource.modified, - function(err) { + function (err) { if (err != null) { logger.err( { diff --git a/app/js/SafeReader.js b/app/js/SafeReader.js index 90082675..f3188791 100644 --- a/app/js/SafeReader.js +++ b/app/js/SafeReader.js @@ -22,9 +22,9 @@ module.exports = SafeReader = { readFile(file, size, encoding, callback) { if (callback == null) { - callback = function(error, result) {} + callback = function (error, result) {} } - return fs.open(file, 'r', function(err, fd) { + return fs.open(file, 'r', function (err, fd) { if (err != null && err.code === 'ENOENT') { return callback() } @@ -34,7 +34,7 @@ module.exports = SafeReader = { // safely return always closing the file const callbackWithClose = (err, ...result) => - fs.close(fd, function(err1) { + fs.close(fd, function (err1) { if (err != null) { return callback(err) } @@ -44,7 +44,7 @@ module.exports = SafeReader = { return callback(null, ...Array.from(result)) }) const buff = Buffer.alloc(size) // fills with zeroes by default - return fs.read(fd, buff, 0, buff.length, 0, function( + return fs.read(fd, buff, 0, buff.length, 0, function ( err, bytesRead, buffer diff --git a/app/js/StaticServerForbidSymlinks.js b/app/js/StaticServerForbidSymlinks.js index 999ae206..edde7774 100644 --- a/app/js/StaticServerForbidSymlinks.js +++ b/app/js/StaticServerForbidSymlinks.js @@ -21,12 +21,12 @@ const Settings = require('settings-sharelatex') const logger = require('logger-sharelatex') const url = require('url') -module.exports = ForbidSymlinks = function(staticFn, root, options) { +module.exports = ForbidSymlinks = function (staticFn, root, options) { const expressStatic = staticFn(root, options) const basePath = Path.resolve(root) - return function(req, res, next) { + return function (req, res, next) { let file, project_id, result - const path = __guard__(url.parse(req.url), x => x.pathname) + const path = __guard__(url.parse(req.url), (x) => x.pathname) // check that the path is of the form /project_id_or_name/path/to/file.log if ((result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/))) { project_id = result[1] @@ -52,7 +52,7 @@ module.exports = ForbidSymlinks = function(staticFn, root, options) { return res.sendStatus(404) } // check that the requested path is not a symlink - return fs.realpath(requestedFsPath, function(err, realFsPath) { + return fs.realpath(requestedFsPath, function (err, realFsPath) { if (err != null) { if (err.code === 'ENOENT') { return res.sendStatus(404) diff --git a/app/js/TikzManager.js b/app/js/TikzManager.js index 3c578735..0e39897d 100644 --- a/app/js/TikzManager.js +++ b/app/js/TikzManager.js @@ -26,7 +26,7 @@ module.exports = TikzManager = { checkMainFile(compileDir, mainFile, resources, callback) { // if there's already an output.tex file, we don't want to touch it if (callback == null) { - callback = function(error, needsMainFile) {} + callback = function (error, needsMainFile) {} } for (const resource of Array.from(resources)) { if (resource.path === 'output.tex') { @@ -35,14 +35,17 @@ module.exports = TikzManager = { } } // if there's no output.tex, see if we are using tikz/pgf or pstool in the main file - return ResourceWriter.checkPath(compileDir, mainFile, function( + return ResourceWriter.checkPath(compileDir, mainFile, function ( error, path ) { if (error != null) { return callback(error) } - return SafeReader.readFile(path, 65536, 'utf8', function(error, content) { + return SafeReader.readFile(path, 65536, 'utf8', function ( + error, + content + ) { if (error != null) { return callback(error) } @@ -64,16 +67,16 @@ module.exports = TikzManager = { injectOutputFile(compileDir, mainFile, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return ResourceWriter.checkPath(compileDir, mainFile, function( + return ResourceWriter.checkPath(compileDir, mainFile, function ( error, path ) { if (error != null) { return callback(error) } - return fs.readFile(path, 'utf8', function(error, content) { + return fs.readFile(path, 'utf8', function (error, content) { if (error != null) { return callback(error) } diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index df9c175a..e8ee10dc 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -25,7 +25,7 @@ const async = require('async') module.exports = UrlCache = { downloadUrlToFile(project_id, url, destPath, lastModified, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } return UrlCache._ensureUrlIsInCache( project_id, @@ -35,7 +35,7 @@ module.exports = UrlCache = { if (error != null) { return callback(error) } - return UrlCache._copyFile(pathToCachedUrl, destPath, function(error) { + return UrlCache._copyFile(pathToCachedUrl, destPath, function (error) { if (error != null) { return UrlCache._clearUrlDetails(project_id, url, () => callback(error) @@ -50,9 +50,9 @@ module.exports = UrlCache = { clearProject(project_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return UrlCache._findAllUrlsInProject(project_id, function(error, urls) { + return UrlCache._findAllUrlsInProject(project_id, function (error, urls) { logger.log( { project_id, url_count: urls.length }, 'clearing project URLs' @@ -60,9 +60,9 @@ module.exports = UrlCache = { if (error != null) { return callback(error) } - const jobs = Array.from(urls || []).map(url => - (url => callback => - UrlCache._clearUrlFromCache(project_id, url, function(error) { + const jobs = Array.from(urls || []).map((url) => + ((url) => (callback) => + UrlCache._clearUrlFromCache(project_id, url, function (error) { if (error != null) { logger.error( { err: error, project_id, url }, @@ -78,7 +78,7 @@ module.exports = UrlCache = { _ensureUrlIsInCache(project_id, url, lastModified, callback) { if (callback == null) { - callback = function(error, pathOnDisk) {} + callback = function (error, pathOnDisk) {} } if (lastModified != null) { // MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds. @@ -98,7 +98,7 @@ module.exports = UrlCache = { return UrlFetcher.pipeUrlToFileWithRetry( url, UrlCache._cacheFilePathForUrl(project_id, url), - error => { + (error) => { if (error != null) { return callback(error) } @@ -106,7 +106,7 @@ module.exports = UrlCache = { project_id, url, lastModified, - error => { + (error) => { if (error != null) { return callback(error) } @@ -128,12 +128,12 @@ module.exports = UrlCache = { _doesUrlNeedDownloading(project_id, url, lastModified, callback) { if (callback == null) { - callback = function(error, needsDownloading) {} + callback = function (error, needsDownloading) {} } if (lastModified == null) { return callback(null, true) } - return UrlCache._findUrlDetails(project_id, url, function( + return UrlCache._findUrlDetails(project_id, url, function ( error, urlDetails ) { @@ -153,14 +153,7 @@ module.exports = UrlCache = { }, _cacheFileNameForUrl(project_id, url) { - return ( - project_id + - ':' + - crypto - .createHash('md5') - .update(url) - .digest('hex') - ) + return project_id + ':' + crypto.createHash('md5').update(url).digest('hex') }, _cacheFilePathForUrl(project_id, url) { @@ -172,14 +165,14 @@ module.exports = UrlCache = { _copyFile(from, to, _callback) { if (_callback == null) { - _callback = function(error) {} + _callback = function (error) {} } - const callbackOnce = function(error) { + const callbackOnce = function (error) { if (error != null) { logger.error({ err: error, from, to }, 'error copying file from cache') } _callback(error) - return (_callback = function() {}) + return (_callback = function () {}) } const writeStream = fs.createWriteStream(to) const readStream = fs.createReadStream(from) @@ -191,13 +184,15 @@ module.exports = UrlCache = { _clearUrlFromCache(project_id, url, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return UrlCache._clearUrlDetails(project_id, url, function(error) { + return UrlCache._clearUrlDetails(project_id, url, function (error) { if (error != null) { return callback(error) } - return UrlCache._deleteUrlCacheFromDisk(project_id, url, function(error) { + return UrlCache._deleteUrlCacheFromDisk(project_id, url, function ( + error + ) { if (error != null) { return callback(error) } @@ -208,9 +203,9 @@ module.exports = UrlCache = { _deleteUrlCacheFromDisk(project_id, url, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function( + return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function ( error ) { if (error != null && error.code !== 'ENOENT') { @@ -224,20 +219,20 @@ module.exports = UrlCache = { _findUrlDetails(project_id, url, callback) { if (callback == null) { - callback = function(error, urlDetails) {} + callback = function (error, urlDetails) {} } - const job = cb => + const job = (cb) => db.UrlCache.findOne({ where: { url, project_id } }) - .then(urlDetails => cb(null, urlDetails)) + .then((urlDetails) => cb(null, urlDetails)) .error(cb) return dbQueue.queue.push(job, callback) }, _updateOrCreateUrlDetails(project_id, url, lastModified, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - const job = cb => + const job = (cb) => db.UrlCache.findOrCreate({ where: { url, project_id } }) .spread((urlDetails, created) => urlDetails @@ -251,9 +246,9 @@ module.exports = UrlCache = { _clearUrlDetails(project_id, url, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - const job = cb => + const job = (cb) => db.UrlCache.destroy({ where: { url, project_id } }) .then(() => cb(null)) .error(cb) @@ -262,14 +257,14 @@ module.exports = UrlCache = { _findAllUrlsInProject(project_id, callback) { if (callback == null) { - callback = function(error, urls) {} + callback = function (error, urls) {} } - const job = cb => + const job = (cb) => db.UrlCache.findAll({ where: { project_id } }) - .then(urlEntries => + .then((urlEntries) => cb( null, - urlEntries.map(entry => entry.url) + urlEntries.map((entry) => entry.url) ) ) .error(cb) diff --git a/app/js/UrlFetcher.js b/app/js/UrlFetcher.js index f9f993f3..b3ed8c46 100644 --- a/app/js/UrlFetcher.js +++ b/app/js/UrlFetcher.js @@ -24,7 +24,7 @@ const oneMinute = 60 * 1000 module.exports = UrlFetcher = { pipeUrlToFileWithRetry(url, filePath, callback) { - const doDownload = function(cb) { + const doDownload = function (cb) { UrlFetcher.pipeUrlToFile(url, filePath, cb) } async.retry(3, doDownload, callback) @@ -32,14 +32,14 @@ module.exports = UrlFetcher = { pipeUrlToFile(url, filePath, _callback) { if (_callback == null) { - _callback = function(error) {} + _callback = function (error) {} } - const callbackOnce = function(error) { + const callbackOnce = function (error) { if (timeoutHandler != null) { clearTimeout(timeoutHandler) } _callback(error) - return (_callback = function() {}) + return (_callback = function () {}) } if (settings.filestoreDomainOveride != null) { @@ -47,7 +47,7 @@ module.exports = UrlFetcher = { url = `${settings.filestoreDomainOveride}${p}` } var timeoutHandler = setTimeout( - function() { + function () { timeoutHandler = null logger.error({ url, filePath }, 'Timed out downloading file to cache') return callbackOnce( @@ -63,7 +63,7 @@ module.exports = UrlFetcher = { urlStream.pause() // stop data flowing until we are ready // attach handlers before setting up pipes - urlStream.on('error', function(error) { + urlStream.on('error', function (error) { logger.error({ err: error, url, filePath }, 'error downloading url') return callbackOnce( error || new Error(`Something went wrong downloading the URL ${url}`) @@ -74,17 +74,17 @@ module.exports = UrlFetcher = { logger.log({ url, filePath }, 'finished downloading file into cache') ) - return urlStream.on('response', function(res) { + return urlStream.on('response', function (res) { if (res.statusCode >= 200 && res.statusCode < 300) { const fileStream = fs.createWriteStream(filePath) // attach handlers before setting up pipes - fileStream.on('error', function(error) { + fileStream.on('error', function (error) { logger.error( { err: error, url, filePath }, 'error writing file into cache' ) - return fs.unlink(filePath, function(err) { + return fs.unlink(filePath, function (err) { if (err != null) { logger.err({ err, filePath }, 'error deleting file from cache') } @@ -92,7 +92,7 @@ module.exports = UrlFetcher = { }) }) - fileStream.on('finish', function() { + fileStream.on('finish', function () { logger.log({ url, filePath }, 'finished writing file into cache') return callbackOnce() }) diff --git a/app/js/db.js b/app/js/db.js index 15510ae3..6b7e50ec 100644 --- a/app/js/db.js +++ b/app/js/db.js @@ -62,6 +62,6 @@ module.exports = { return sequelize .sync() .then(() => logger.log('db sync complete')) - .catch(err => console.log(err, 'error syncing')) + .catch((err) => console.log(err, 'error syncing')) } } diff --git a/buildscript.txt b/buildscript.txt index 78ef1d04..b2866578 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,11 +1,9 @@ clsi ---acceptance-creds=None --data-dirs=cache,compiles,db --dependencies= --docker-repos=gcr.io/overleaf-ops --env-add= --env-pass-through=TEXLIVE_IMAGE ---language=es --node-version=10.21.0 --public-repo=True ---script-version=2.1.0 +--script-version=3.3.2 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index facbd5fc..35f48780 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -10,6 +10,7 @@ services: command: npm run test:unit:_run environment: NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" test_acceptance: @@ -25,6 +26,7 @@ services: POSTGRES_HOST: postgres MOCHA_GREP: ${MOCHA_GREP} NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" TEXLIVE_IMAGE: command: npm run test:acceptance:_run diff --git a/docker-compose.yml b/docker-compose.yml index 6fc9eab2..052d9098 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,8 @@ services: environment: MOCHA_GREP: ${MOCHA_GREP} NODE_ENV: test - command: npm run test:unit + NODE_OPTIONS: "--unhandled-rejections=strict" + command: npm run --silent test:unit test_acceptance: build: @@ -35,5 +36,6 @@ services: MOCHA_GREP: ${MOCHA_GREP} LOG_LEVEL: ERROR NODE_ENV: test - command: npm run test:acceptance + NODE_OPTIONS: "--unhandled-rejections=strict" + command: npm run --silent test:acceptance diff --git a/nodemon.json b/nodemon.json index 5826281b..e3e8817d 100644 --- a/nodemon.json +++ b/nodemon.json @@ -8,7 +8,6 @@ "execMap": { "js": "npm run start" }, - "watch": [ "app/js/", "app.js", diff --git a/package-lock.json b/package-lock.json index 63d9c1e7..cc358212 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4770,9 +4770,9 @@ "dev": true }, "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", "dev": true }, "prettier-eslint": { @@ -5004,6 +5004,12 @@ "mimic-fn": "^1.0.0" } }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", diff --git a/package.json b/package.json index df30ef77..a5260759 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", - "lint": "node_modules/.bin/eslint .", + "lint": "node_modules/.bin/eslint --max-warnings 0 .", "format": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --list-different", "format:fix": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --write" }, @@ -59,7 +59,7 @@ "eslint-plugin-react": "^7.19.0", "eslint-plugin-standard": "^4.0.1", "mocha": "^7.1.0", - "prettier": "^1.19.1", + "prettier": "^2.0.0", "prettier-eslint-cli": "^5.0.0", "sandboxed-module": "^2.0.3", "sinon": "~9.0.1", diff --git a/test/acceptance/js/AllowedImageNames.js b/test/acceptance/js/AllowedImageNames.js index a9b3996e..7e38954e 100644 --- a/test/acceptance/js/AllowedImageNames.js +++ b/test/acceptance/js/AllowedImageNames.js @@ -2,8 +2,8 @@ const Client = require('./helpers/Client') const ClsiApp = require('./helpers/ClsiApp') const { expect } = require('chai') -describe('AllowedImageNames', function() { - beforeEach(function(done) { +describe('AllowedImageNames', function () { + beforeEach(function (done) { this.project_id = Client.randomId() this.request = { options: { @@ -24,8 +24,8 @@ Hello world ClsiApp.ensureRunning(done) }) - describe('with a valid name', function() { - beforeEach(function(done) { + describe('with a valid name', function () { + beforeEach(function (done) { this.request.options.imageName = process.env.TEXLIVE_IMAGE Client.compile(this.project_id, this.request, (error, res, body) => { @@ -35,11 +35,11 @@ Hello world done(error) }) }) - it('should return success', function() { + it('should return success', function () { expect(this.res.statusCode).to.equal(200) }) - it('should return a PDF', function() { + it('should return a PDF', function () { let pdf try { pdf = Client.getOutputFile(this.body, 'pdf') @@ -48,8 +48,8 @@ Hello world }) }) - describe('with an invalid name', function() { - beforeEach(function(done) { + describe('with an invalid name', function () { + beforeEach(function (done) { this.request.options.imageName = 'something/evil:1337' Client.compile(this.project_id, this.request, (error, res, body) => { this.error = error @@ -58,11 +58,11 @@ Hello world done(error) }) }) - it('should return non success', function() { + it('should return non success', function () { expect(this.res.statusCode).to.not.equal(200) }) - it('should not return a PDF', function() { + it('should not return a PDF', function () { let pdf try { pdf = Client.getOutputFile(this.body, 'pdf') @@ -71,11 +71,11 @@ Hello world }) }) - describe('wordcount', function() { - beforeEach(function(done) { + describe('wordcount', function () { + beforeEach(function (done) { Client.compile(this.project_id, this.request, done) }) - it('should error out with an invalid imageName', function() { + it('should error out with an invalid imageName', function () { Client.wordcountWithImage( this.project_id, 'main.tex', @@ -86,7 +86,7 @@ Hello world ) }) - it('should produce a texcout a valid imageName', function() { + it('should produce a texcout a valid imageName', function () { Client.wordcountWithImage( this.project_id, 'main.tex', diff --git a/test/acceptance/js/BrokenLatexFileTests.js b/test/acceptance/js/BrokenLatexFileTests.js index b34d23ca..c26e0c40 100644 --- a/test/acceptance/js/BrokenLatexFileTests.js +++ b/test/acceptance/js/BrokenLatexFileTests.js @@ -13,8 +13,8 @@ const request = require('request') require('chai').should() const ClsiApp = require('./helpers/ClsiApp') -describe('Broken LaTeX file', function() { - before(function(done) { +describe('Broken LaTeX file', function () { + before(function (done) { this.broken_request = { resources: [ { @@ -44,8 +44,8 @@ Hello world return ClsiApp.ensureRunning(done) }) - describe('on first run', function() { - before(function(done) { + describe('on first run', function () { + before(function (done) { this.project_id = Client.randomId() return Client.compile( this.project_id, @@ -59,13 +59,13 @@ Hello world ) }) - return it('should return a failure status', function() { + return it('should return a failure status', function () { return this.body.compile.status.should.equal('failure') }) }) - return describe('on second run', function() { - before(function(done) { + return describe('on second run', function () { + before(function (done) { this.project_id = Client.randomId() return Client.compile(this.project_id, this.correct_request, () => { return Client.compile( @@ -81,7 +81,7 @@ Hello world }) }) - return it('should return a failure status', function() { + return it('should return a failure status', function () { return this.body.compile.status.should.equal('failure') }) }) diff --git a/test/acceptance/js/DeleteOldFilesTest.js b/test/acceptance/js/DeleteOldFilesTest.js index 83d7c96d..ae2d7c79 100644 --- a/test/acceptance/js/DeleteOldFilesTest.js +++ b/test/acceptance/js/DeleteOldFilesTest.js @@ -13,8 +13,8 @@ const request = require('request') require('chai').should() const ClsiApp = require('./helpers/ClsiApp') -describe('Deleting Old Files', function() { - before(function(done) { +describe('Deleting Old Files', function () { + before(function (done) { this.request = { resources: [ { @@ -31,8 +31,8 @@ Hello world return ClsiApp.ensureRunning(done) }) - return describe('on first run', function() { - before(function(done) { + return describe('on first run', function () { + before(function (done) { this.project_id = Client.randomId() return Client.compile( this.project_id, @@ -46,12 +46,12 @@ Hello world ) }) - it('should return a success status', function() { + it('should return a success status', function () { return this.body.compile.status.should.equal('success') }) - return describe('after file has been deleted', function() { - before(function(done) { + return describe('after file has been deleted', function () { + before(function (done) { this.request.resources = [] return Client.compile( this.project_id, @@ -65,7 +65,7 @@ Hello world ) }) - return it('should return a failure status', function() { + return it('should return a failure status', function () { return this.body.compile.status.should.equal('failure') }) }) diff --git a/test/acceptance/js/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js index 5435a787..5a67a0fe 100644 --- a/test/acceptance/js/ExampleDocumentTests.js +++ b/test/acceptance/js/ExampleDocumentTests.js @@ -24,7 +24,7 @@ const ChildProcess = require('child_process') const ClsiApp = require('./helpers/ClsiApp') const logger = require('logger-sharelatex') const Path = require('path') -const fixturePath = path => { +const fixturePath = (path) => { if (path.slice(0, 3) === 'tmp') { return '/tmp/clsi_acceptance_tests' + path.slice(3) } @@ -41,23 +41,23 @@ console.log( const MOCHA_LATEX_TIMEOUT = 60 * 1000 -const convertToPng = function(pdfPath, pngPath, callback) { +const convertToPng = function (pdfPath, pngPath, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}` console.log('COMMAND') console.log(command) const convert = ChildProcess.exec(command) const stdout = '' - convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString())) - convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) + convert.stdout.on('data', (chunk) => console.log('STDOUT', chunk.toString())) + convert.stderr.on('data', (chunk) => console.log('STDERR', chunk.toString())) return convert.on('exit', () => callback()) } -const compare = function(originalPath, generatedPath, callback) { +const compare = function (originalPath, generatedPath, callback) { if (callback == null) { - callback = function(error, same) {} + callback = function (error, same) {} } const diff_file = `${fixturePath(generatedPath)}-diff.png` const proc = ChildProcess.exec( @@ -66,11 +66,11 @@ const compare = function(originalPath, generatedPath, callback) { )} ${diff_file}` ) let stderr = '' - proc.stderr.on('data', chunk => (stderr += chunk)) + proc.stderr.on('data', (chunk) => (stderr += chunk)) return proc.on('exit', () => { if (stderr.trim() === '0 (0)') { // remove output diff if test matches expected image - fs.unlink(diff_file, err => { + fs.unlink(diff_file, (err) => { if (err) { throw err } @@ -83,14 +83,14 @@ const compare = function(originalPath, generatedPath, callback) { }) } -const checkPdfInfo = function(pdfPath, callback) { +const checkPdfInfo = function (pdfPath, callback) { if (callback == null) { - callback = function(error, output) {} + callback = function (error, output) {} } const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`) let stdout = '' - proc.stdout.on('data', chunk => (stdout += chunk)) - proc.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) + proc.stdout.on('data', (chunk) => (stdout += chunk)) + proc.stderr.on('data', (chunk) => console.log('STDERR', chunk.toString())) return proc.on('exit', () => { if (stdout.match(/Optimized:\s+yes/)) { return callback(null, true) @@ -100,11 +100,11 @@ const checkPdfInfo = function(pdfPath, callback) { }) } -const compareMultiplePages = function(project_id, callback) { +const compareMultiplePages = function (project_id, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } - var compareNext = function(page_no, callback) { + var compareNext = function (page_no, callback) { const path = `tmp/${project_id}-source-${page_no}.png` return fs.stat(fixturePath(path), (error, stat) => { if (error != null) { @@ -127,23 +127,23 @@ const compareMultiplePages = function(project_id, callback) { return compareNext(0, callback) } -const comparePdf = function(project_id, example_dir, callback) { +const comparePdf = function (project_id, example_dir, callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } console.log('CONVERT') console.log(`tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`) return convertToPng( `tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`, - error => { + (error) => { if (error != null) { throw error } return convertToPng( `examples/${example_dir}/output.pdf`, `tmp/${project_id}-source.png`, - error => { + (error) => { if (error != null) { throw error } @@ -163,7 +163,7 @@ const comparePdf = function(project_id, example_dir, callback) { } ) } else { - return compareMultiplePages(project_id, error => { + return compareMultiplePages(project_id, (error) => { if (error != null) { throw error } @@ -178,9 +178,14 @@ const comparePdf = function(project_id, example_dir, callback) { ) } -const downloadAndComparePdf = function(project_id, example_dir, url, callback) { +const downloadAndComparePdf = function ( + project_id, + example_dir, + url, + callback +) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } const writeStream = fs.createWriteStream(fixturePath(`tmp/${project_id}.pdf`)) request.get(url).pipe(writeStream) @@ -198,85 +203,96 @@ const downloadAndComparePdf = function(project_id, example_dir, url, callback) { Client.runServer(4242, fixturePath('examples')) -describe('Example Documents', function() { - before(function(done) { +describe('Example Documents', function () { + before(function (done) { ClsiApp.ensureRunning(done) }) - before(function(done) { + before(function (done) { fsExtra.remove(fixturePath('tmp'), done) }) - before(function(done) { + before(function (done) { fs.mkdir(fixturePath('tmp'), done) }) - after(function(done) { + after(function (done) { fsExtra.remove(fixturePath('tmp'), done) }) - return Array.from(fs.readdirSync(fixturePath('examples'))).map(example_dir => - (example_dir => - describe(example_dir, function() { - before(function() { - return (this.project_id = Client.randomId() + '_' + example_dir) - }) + return Array.from(fs.readdirSync(fixturePath('examples'))).map( + (example_dir) => + ((example_dir) => + describe(example_dir, function () { + before(function () { + return (this.project_id = Client.randomId() + '_' + example_dir) + }) - it('should generate the correct pdf', function(done) { - this.timeout(MOCHA_LATEX_TIMEOUT) - return Client.compileDirectory( - this.project_id, - fixturePath('examples'), - example_dir, - 4242, - (error, res, body) => { - if ( - error || - __guard__( - body != null ? body.compile : undefined, - x => x.status - ) === 'failure' - ) { - console.log('DEBUG: error', error, 'body', JSON.stringify(body)) - return done(new Error('Compile failed')) + it('should generate the correct pdf', function (done) { + this.timeout(MOCHA_LATEX_TIMEOUT) + return Client.compileDirectory( + this.project_id, + fixturePath('examples'), + example_dir, + 4242, + (error, res, body) => { + if ( + error || + __guard__( + body != null ? body.compile : undefined, + (x) => x.status + ) === 'failure' + ) { + console.log( + 'DEBUG: error', + error, + 'body', + JSON.stringify(body) + ) + return done(new Error('Compile failed')) + } + const pdf = Client.getOutputFile(body, 'pdf') + return downloadAndComparePdf( + this.project_id, + example_dir, + pdf.url, + done + ) } - const pdf = Client.getOutputFile(body, 'pdf') - return downloadAndComparePdf( - this.project_id, - example_dir, - pdf.url, - done - ) - } - ) - }) + ) + }) - return it('should generate the correct pdf on the second run as well', function(done) { - this.timeout(MOCHA_LATEX_TIMEOUT) - return Client.compileDirectory( - this.project_id, - fixturePath('examples'), - example_dir, - 4242, - (error, res, body) => { - if ( - error || - __guard__( - body != null ? body.compile : undefined, - x => x.status - ) === 'failure' - ) { - console.log('DEBUG: error', error, 'body', JSON.stringify(body)) - return done(new Error('Compile failed')) + return it('should generate the correct pdf on the second run as well', function (done) { + this.timeout(MOCHA_LATEX_TIMEOUT) + return Client.compileDirectory( + this.project_id, + fixturePath('examples'), + example_dir, + 4242, + (error, res, body) => { + if ( + error || + __guard__( + body != null ? body.compile : undefined, + (x) => x.status + ) === 'failure' + ) { + console.log( + 'DEBUG: error', + error, + 'body', + JSON.stringify(body) + ) + return done(new Error('Compile failed')) + } + const pdf = Client.getOutputFile(body, 'pdf') + return downloadAndComparePdf( + this.project_id, + example_dir, + pdf.url, + done + ) } - const pdf = Client.getOutputFile(body, 'pdf') - return downloadAndComparePdf( - this.project_id, - example_dir, - pdf.url, - done - ) - } - ) - }) - }))(example_dir) + ) + }) + }))(example_dir) ) }) diff --git a/test/acceptance/js/SimpleLatexFileTests.js b/test/acceptance/js/SimpleLatexFileTests.js index 447e1b63..dd458026 100644 --- a/test/acceptance/js/SimpleLatexFileTests.js +++ b/test/acceptance/js/SimpleLatexFileTests.js @@ -13,8 +13,8 @@ const request = require('request') require('chai').should() const ClsiApp = require('./helpers/ClsiApp') -describe('Simple LaTeX file', function() { - before(function(done) { +describe('Simple LaTeX file', function () { + before(function (done) { this.project_id = Client.randomId() this.request = { resources: [ @@ -43,17 +43,17 @@ Hello world }) }) - it('should return the PDF', function() { + it('should return the PDF', function () { const pdf = Client.getOutputFile(this.body, 'pdf') return pdf.type.should.equal('pdf') }) - it('should return the log', function() { + it('should return the log', function () { const log = Client.getOutputFile(this.body, 'log') return log.type.should.equal('log') }) - it('should provide the pdf for download', function(done) { + it('should provide the pdf for download', function (done) { const pdf = Client.getOutputFile(this.body, 'pdf') return request.get(pdf.url, (error, res, body) => { res.statusCode.should.equal(200) @@ -61,7 +61,7 @@ Hello world }) }) - return it('should provide the log for download', function(done) { + return it('should provide the log for download', function (done) { const log = Client.getOutputFile(this.body, 'pdf') return request.get(log.url, (error, res, body) => { res.statusCode.should.equal(200) diff --git a/test/acceptance/js/SynctexTests.js b/test/acceptance/js/SynctexTests.js index 1140f3fa..afe6330d 100644 --- a/test/acceptance/js/SynctexTests.js +++ b/test/acceptance/js/SynctexTests.js @@ -16,8 +16,8 @@ const { expect } = require('chai') const ClsiApp = require('./helpers/ClsiApp') const crypto = require('crypto') -describe('Syncing', function() { - before(function(done) { +describe('Syncing', function () { + before(function (done) { const content = `\ \\documentclass{article} \\begin{document} @@ -47,8 +47,8 @@ Hello world }) }) - describe('from code to pdf', function() { - return it('should return the correct location', function(done) { + describe('from code to pdf', function () { + return it('should return the correct location', function (done) { return Client.syncFromCode( this.project_id, 'main.tex', @@ -69,8 +69,8 @@ Hello world }) }) - describe('from pdf to code', function() { - return it('should return the correct location', function(done) { + describe('from pdf to code', function () { + return it('should return the correct location', function (done) { return Client.syncFromPdf( this.project_id, 1, @@ -89,12 +89,12 @@ Hello world }) }) - describe('when the project directory is not available', function() { - before(function() { + describe('when the project directory is not available', function () { + before(function () { this.other_project_id = Client.randomId() }) - describe('from code to pdf', function() { - it('should return a 404 response', function(done) { + describe('from code to pdf', function () { + it('should return a 404 response', function (done) { return Client.syncFromCode( this.other_project_id, 'main.tex', @@ -110,8 +110,8 @@ Hello world ) }) }) - describe('from pdf to code', function() { - it('should return a 404 response', function(done) { + describe('from pdf to code', function () { + it('should return a 404 response', function (done) { return Client.syncFromPdf( this.other_project_id, 1, @@ -129,8 +129,8 @@ Hello world }) }) - describe('when the synctex file is not available', function() { - before(function(done) { + describe('when the synctex file is not available', function () { + before(function (done) { this.broken_project_id = Client.randomId() const content = 'this is not valid tex' // not a valid tex file this.request = { @@ -153,8 +153,8 @@ Hello world ) }) - describe('from code to pdf', function() { - it('should return a 404 response', function(done) { + describe('from code to pdf', function () { + it('should return a 404 response', function (done) { return Client.syncFromCode( this.broken_project_id, 'main.tex', @@ -170,8 +170,8 @@ Hello world ) }) }) - describe('from pdf to code', function() { - it('should return a 404 response', function(done) { + describe('from pdf to code', function () { + it('should return a 404 response', function (done) { return Client.syncFromPdf( this.broken_project_id, 1, diff --git a/test/acceptance/js/TimeoutTests.js b/test/acceptance/js/TimeoutTests.js index f6812e8c..36bcf9aa 100644 --- a/test/acceptance/js/TimeoutTests.js +++ b/test/acceptance/js/TimeoutTests.js @@ -13,8 +13,8 @@ const request = require('request') require('chai').should() const ClsiApp = require('./helpers/ClsiApp') -describe('Timed out compile', function() { - before(function(done) { +describe('Timed out compile', function () { + before(function (done) { this.request = { options: { timeout: 10 @@ -47,16 +47,16 @@ describe('Timed out compile', function() { }) }) - it('should return a timeout error', function() { + it('should return a timeout error', function () { return this.body.compile.error.should.equal('container timed out') }) - it('should return a timedout status', function() { + it('should return a timedout status', function () { return this.body.compile.status.should.equal('timedout') }) - return it('should return the log output file name', function() { - const outputFilePaths = this.body.compile.outputFiles.map(x => x.path) + return it('should return the log output file name', function () { + const outputFilePaths = this.body.compile.outputFiles.map((x) => x.path) return outputFilePaths.should.include('output.log') }) }) diff --git a/test/acceptance/js/UrlCachingTests.js b/test/acceptance/js/UrlCachingTests.js index b86541b6..03927ea1 100644 --- a/test/acceptance/js/UrlCachingTests.js +++ b/test/acceptance/js/UrlCachingTests.js @@ -35,17 +35,15 @@ const Server = { getFile() {}, randomId() { - return Math.random() - .toString(16) - .slice(2) + return Math.random().toString(16).slice(2) } } Server.run() -describe('Url Caching', function() { - describe('Downloading an image for the first time', function() { - before(function(done) { +describe('Url Caching', function () { + describe('Downloading an image for the first time', function () { + before(function (done) { this.project_id = Client.randomId() this.file = `${Server.randomId()}/lion.png` this.request = { @@ -82,17 +80,17 @@ describe('Url Caching', function() { }) }) - afterEach(function() { + afterEach(function () { return Server.getFile.restore() }) - return it('should download the image', function() { + return it('should download the image', function () { return Server.getFile.calledWith(`/${this.file}`).should.equal(true) }) }) - describe('When an image is in the cache and the last modified date is unchanged', function() { - before(function(done) { + describe('When an image is in the cache and the last modified date is unchanged', function () { + before(function (done) { this.project_id = Client.randomId() this.file = `${Server.randomId()}/lion.png` this.request = { @@ -137,17 +135,17 @@ describe('Url Caching', function() { ) }) - after(function() { + after(function () { return Server.getFile.restore() }) - return it('should not download the image again', function() { + return it('should not download the image again', function () { return Server.getFile.called.should.equal(false) }) }) - describe('When an image is in the cache and the last modified date is advanced', function() { - before(function(done) { + describe('When an image is in the cache and the last modified date is advanced', function () { + before(function (done) { this.project_id = Client.randomId() this.file = `${Server.randomId()}/lion.png` this.request = { @@ -193,17 +191,17 @@ describe('Url Caching', function() { ) }) - afterEach(function() { + afterEach(function () { return Server.getFile.restore() }) - return it('should download the image again', function() { + return it('should download the image again', function () { return Server.getFile.called.should.equal(true) }) }) - describe('When an image is in the cache and the last modified date is further in the past', function() { - before(function(done) { + describe('When an image is in the cache and the last modified date is further in the past', function () { + before(function (done) { this.project_id = Client.randomId() this.file = `${Server.randomId()}/lion.png` this.request = { @@ -249,17 +247,17 @@ describe('Url Caching', function() { ) }) - afterEach(function() { + afterEach(function () { return Server.getFile.restore() }) - return it('should not download the image again', function() { + return it('should not download the image again', function () { return Server.getFile.called.should.equal(false) }) }) - describe('When an image is in the cache and the last modified date is not specified', function() { - before(function(done) { + describe('When an image is in the cache and the last modified date is not specified', function () { + before(function (done) { this.project_id = Client.randomId() this.file = `${Server.randomId()}/lion.png` this.request = { @@ -305,17 +303,17 @@ describe('Url Caching', function() { ) }) - afterEach(function() { + afterEach(function () { return Server.getFile.restore() }) - return it('should download the image again', function() { + return it('should download the image again', function () { return Server.getFile.called.should.equal(true) }) }) - return describe('After clearing the cache', function() { - before(function(done) { + return describe('After clearing the cache', function () { + before(function (done) { this.project_id = Client.randomId() this.file = `${Server.randomId()}/lion.png` this.request = { @@ -338,7 +336,7 @@ describe('Url Caching', function() { ] } - return Client.compile(this.project_id, this.request, error => { + return Client.compile(this.project_id, this.request, (error) => { if (error != null) { throw error } @@ -361,11 +359,11 @@ describe('Url Caching', function() { }) }) - afterEach(function() { + afterEach(function () { return Server.getFile.restore() }) - return it('should download the image again', function() { + return it('should download the image again', function () { return Server.getFile.called.should.equal(true) }) }) diff --git a/test/acceptance/js/WordcountTests.js b/test/acceptance/js/WordcountTests.js index 87218570..73a11f76 100644 --- a/test/acceptance/js/WordcountTests.js +++ b/test/acceptance/js/WordcountTests.js @@ -17,8 +17,8 @@ const path = require('path') const fs = require('fs') const ClsiApp = require('./helpers/ClsiApp') -describe('Syncing', function() { - before(function(done) { +describe('Syncing', function () { + before(function (done) { this.request = { resources: [ { @@ -45,8 +45,8 @@ describe('Syncing', function() { }) }) - return describe('wordcount file', function() { - return it('should return wordcount info', function(done) { + return describe('wordcount file', function () { + return it('should return wordcount info', function (done) { return Client.wordcount(this.project_id, 'main.tex', (error, result) => { if (error != null) { throw error diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index c940e305..7cd0c114 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -23,14 +23,12 @@ module.exports = Client = { host: Settings.apis.clsi.url, randomId() { - return Math.random() - .toString(16) - .slice(2) + return Math.random().toString(16).slice(2) }, compile(project_id, data, callback) { if (callback == null) { - callback = function(error, res, body) {} + callback = function (error, res, body) {} } return request.post( { @@ -45,7 +43,7 @@ module.exports = Client = { clearCache(project_id, callback) { if (callback == null) { - callback = function(error, res, body) {} + callback = function (error, res, body) {} } return request.del(`${this.host}/project/${project_id}`, callback) }, @@ -64,7 +62,7 @@ module.exports = Client = { const app = express() app.use(express.static(directory)) console.log('starting test server on', port, host) - return app.listen(port, host).on('error', error => { + return app.listen(port, host).on('error', (error) => { console.error('error starting server:', error.message) return process.exit(1) }) @@ -72,7 +70,7 @@ module.exports = Client = { syncFromCode(project_id, file, line, column, callback) { if (callback == null) { - callback = function(error, pdfPositions) {} + callback = function (error, pdfPositions) {} } return request.get( { @@ -95,7 +93,7 @@ module.exports = Client = { syncFromPdf(project_id, page, h, v, callback) { if (callback == null) { - callback = function(error, pdfPositions) {} + callback = function (error, pdfPositions) {} } return request.get( { @@ -118,7 +116,7 @@ module.exports = Client = { compileDirectory(project_id, baseDirectory, directory, serverPort, callback) { if (callback == null) { - callback = function(error, res, body) {} + callback = function (error, res, body) {} } const resources = [] let entities = fs.readdirSync(`${baseDirectory}/${directory}`) @@ -130,7 +128,7 @@ module.exports = Client = { entities = entities.concat( fs .readdirSync(`${baseDirectory}/${directory}/${entity}`) - .map(subEntity => { + .map((subEntity) => { if (subEntity === 'main.tex') { rootResourcePath = `${entity}/${subEntity}` } @@ -195,7 +193,7 @@ module.exports = Client = { wordcountWithImage(project_id, file, image, callback) { if (callback == null) { - callback = function(error, pdfPositions) {} + callback = function (error, pdfPositions) {} } return request.get( { diff --git a/test/acceptance/js/helpers/ClsiApp.js b/test/acceptance/js/helpers/ClsiApp.js index f8038460..160b07e5 100644 --- a/test/acceptance/js/helpers/ClsiApp.js +++ b/test/acceptance/js/helpers/ClsiApp.js @@ -23,7 +23,7 @@ module.exports = { callbacks: [], ensureRunning(callback) { if (callback == null) { - callback = function(error) {} + callback = function (error) {} } if (this.running) { return callback() @@ -35,10 +35,10 @@ module.exports = { return app.listen( __guard__( Settings.internal != null ? Settings.internal.clsi : undefined, - x => x.port + (x) => x.port ), 'localhost', - error => { + (error) => { if (error != null) { throw error } diff --git a/test/load/js/loadTest.js b/test/load/js/loadTest.js index c4116a2a..65090b8b 100644 --- a/test/load/js/loadTest.js +++ b/test/load/js/loadTest.js @@ -17,7 +17,7 @@ const _ = require('lodash') const concurentCompiles = 5 const totalCompiles = 50 -const buildUrl = path => +const buildUrl = (path) => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}` const mainTexContent = fs.readFileSync('./bulk.tex', 'utf-8') @@ -25,12 +25,12 @@ const mainTexContent = fs.readFileSync('./bulk.tex', 'utf-8') const compileTimes = [] let failedCount = 0 -const getAverageCompileTime = function() { +const getAverageCompileTime = function () { const totalTime = _.reduce(compileTimes, (sum, time) => sum + time, 0) return totalTime / compileTimes.length } -const makeRequest = function(compileNumber, callback) { +const makeRequest = function (compileNumber, callback) { let bulkBodyCount = 7 let bodyContent = '' while (--bulkBodyCount) { @@ -74,12 +74,12 @@ ${bodyContent} ) } -const jobs = _.map(__range__(1, totalCompiles, true), i => cb => +const jobs = _.map(__range__(1, totalCompiles, true), (i) => (cb) => makeRequest(i, cb) ) const startTime = new Date() -async.parallelLimit(jobs, concurentCompiles, err => { +async.parallelLimit(jobs, concurentCompiles, (err) => { if (err != null) { console.error(err) } diff --git a/test/smoke/js/SmokeTests.js b/test/smoke/js/SmokeTests.js index fc52121e..8cfb190b 100644 --- a/test/smoke/js/SmokeTests.js +++ b/test/smoke/js/SmokeTests.js @@ -1,20 +1,20 @@ const request = require('request') const Settings = require('settings-sharelatex') -const buildUrl = path => +const buildUrl = (path) => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}` const url = buildUrl(`project/smoketest-${process.pid}/compile`) module.exports = { sendNewResult(res) { - this._run(error => this._sendResponse(res, error)) + this._run((error) => this._sendResponse(res, error)) }, sendLastResult(res) { this._sendResponse(res, this._lastError) }, triggerRun(cb) { - this._run(error => { + this._run((error) => { this._lastError = error cb(error) }) diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 8bb83e66..1c93c96e 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -19,8 +19,8 @@ const modulePath = require('path').join( ) const tk = require('timekeeper') -describe('CompileController', function() { - beforeEach(function() { +describe('CompileController', function () { + beforeEach(function () { this.CompileController = SandboxedModule.require(modulePath, { requires: { './CompileManager': (this.CompileManager = {}), @@ -47,8 +47,8 @@ describe('CompileController', function() { return (this.next = sinon.stub()) }) - describe('compile', function() { - beforeEach(function() { + describe('compile', function () { + beforeEach(function () { this.req.body = { compile: 'mock-body' } @@ -82,40 +82,40 @@ describe('CompileController', function() { return (this.res.send = sinon.stub()) }) - describe('successfully', function() { - beforeEach(function() { + describe('successfully', function () { + beforeEach(function () { this.CompileManager.doCompileWithLock = sinon .stub() .callsArgWith(1, null, this.output_files) return this.CompileController.compile(this.req, this.res) }) - it('should parse the request', function() { + it('should parse the request', function () { return this.RequestParser.parse .calledWith(this.req.body) .should.equal(true) }) - it('should run the compile for the specified project', function() { + it('should run the compile for the specified project', function () { return this.CompileManager.doCompileWithLock .calledWith(this.request_with_project_id) .should.equal(true) }) - it('should mark the project as accessed', function() { + it('should mark the project as accessed', function () { return this.ProjectPersistenceManager.markProjectAsJustAccessed .calledWith(this.project_id) .should.equal(true) }) - return it('should return the JSON response', function() { + return it('should return the JSON response', function () { this.res.status.calledWith(200).should.equal(true) return this.res.send .calledWith({ compile: { status: 'success', error: null, - outputFiles: this.output_files.map(file => { + outputFiles: this.output_files.map((file) => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, path: file.path, @@ -129,15 +129,15 @@ describe('CompileController', function() { }) }) - describe('with an error', function() { - beforeEach(function() { + describe('with an error', function () { + beforeEach(function () { this.CompileManager.doCompileWithLock = sinon .stub() .callsArgWith(1, new Error((this.message = 'error message')), null) return this.CompileController.compile(this.req, this.res) }) - return it('should return the JSON response with the error', function() { + return it('should return the JSON response with the error', function () { this.res.status.calledWith(500).should.equal(true) return this.res.send .calledWith({ @@ -151,8 +151,8 @@ describe('CompileController', function() { }) }) - describe('when the request times out', function() { - beforeEach(function() { + describe('when the request times out', function () { + beforeEach(function () { this.error = new Error((this.message = 'container timed out')) this.error.timedout = true this.CompileManager.doCompileWithLock = sinon @@ -161,7 +161,7 @@ describe('CompileController', function() { return this.CompileController.compile(this.req, this.res) }) - return it('should return the JSON response with the timeout status', function() { + return it('should return the JSON response with the timeout status', function () { this.res.status.calledWith(200).should.equal(true) return this.res.send .calledWith({ @@ -175,15 +175,15 @@ describe('CompileController', function() { }) }) - return describe('when the request returns no output files', function() { - beforeEach(function() { + return describe('when the request returns no output files', function () { + beforeEach(function () { this.CompileManager.doCompileWithLock = sinon .stub() .callsArgWith(1, null, []) return this.CompileController.compile(this.req, this.res) }) - return it('should return the JSON response with the failure status', function() { + return it('should return the JSON response with the failure status', function () { this.res.status.calledWith(200).should.equal(true) return this.res.send .calledWith({ @@ -198,8 +198,8 @@ describe('CompileController', function() { }) }) - describe('syncFromCode', function() { - beforeEach(function() { + describe('syncFromCode', function () { + beforeEach(function () { this.file = 'main.tex' this.line = 42 this.column = 5 @@ -218,7 +218,7 @@ describe('CompileController', function() { return this.CompileController.syncFromCode(this.req, this.res, this.next) }) - it('should find the corresponding location in the PDF', function() { + it('should find the corresponding location in the PDF', function () { return this.CompileManager.syncFromCode .calledWith( this.project_id, @@ -230,7 +230,7 @@ describe('CompileController', function() { .should.equal(true) }) - return it('should return the positions', function() { + return it('should return the positions', function () { return this.res.json .calledWith({ pdf: this.pdfPositions @@ -239,8 +239,8 @@ describe('CompileController', function() { }) }) - describe('syncFromPdf', function() { - beforeEach(function() { + describe('syncFromPdf', function () { + beforeEach(function () { this.page = 5 this.h = 100.23 this.v = 45.67 @@ -259,13 +259,13 @@ describe('CompileController', function() { return this.CompileController.syncFromPdf(this.req, this.res, this.next) }) - it('should find the corresponding location in the code', function() { + it('should find the corresponding location in the code', function () { return this.CompileManager.syncFromPdf .calledWith(this.project_id, undefined, this.page, this.h, this.v) .should.equal(true) }) - return it('should return the positions', function() { + return it('should return the positions', function () { return this.res.json .calledWith({ code: this.codePositions @@ -274,8 +274,8 @@ describe('CompileController', function() { }) }) - return describe('wordcount', function() { - beforeEach(function() { + return describe('wordcount', function () { + beforeEach(function () { this.file = 'main.tex' this.project_id = 'mock-project-id' this.req.params = { project_id: this.project_id } @@ -290,14 +290,14 @@ describe('CompileController', function() { .callsArgWith(4, null, (this.texcount = ['mock-texcount'])) }) - it('should return the word count of a file', function() { + it('should return the word count of a file', function () { this.CompileController.wordcount(this.req, this.res, this.next) return this.CompileManager.wordcount .calledWith(this.project_id, undefined, this.file, this.image) .should.equal(true) }) - it('should return the texcount info', function() { + it('should return the texcount info', function () { this.CompileController.wordcount(this.req, this.res, this.next) return this.res.json .calledWith({ @@ -306,8 +306,8 @@ describe('CompileController', function() { .should.equal(true) }) - describe('when allowedImages is set', function() { - beforeEach(function() { + describe('when allowedImages is set', function () { + beforeEach(function () { this.Settings.clsi = { docker: {} } this.Settings.clsi.docker.allowedImages = [ 'repo/image:tag1', @@ -317,28 +317,28 @@ describe('CompileController', function() { this.res.status = sinon.stub().returns({ send: this.res.send }) }) - describe('with an invalid image', function() { - beforeEach(function() { + describe('with an invalid image', function () { + beforeEach(function () { this.req.query.image = 'something/evil:1337' this.CompileController.wordcount(this.req, this.res, this.next) }) - it('should return a 400', function() { + it('should return a 400', function () { expect(this.res.status.calledWith(400)).to.equal(true) }) - it('should not run the query', function() { + it('should not run the query', function () { expect(this.CompileManager.wordcount.called).to.equal(false) }) }) - describe('with a valid image', function() { - beforeEach(function() { + describe('with a valid image', function () { + beforeEach(function () { this.req.query.image = 'repo/image:tag1' this.CompileController.wordcount(this.req, this.res, this.next) }) - it('should not return a 400', function() { + it('should not return a 400', function () { expect(this.res.status.calledWith(400)).to.equal(false) }) - it('should run the query', function() { + it('should run the query', function () { expect(this.CompileManager.wordcount.called).to.equal(true) }) }) diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index 90d572b2..cb85f2f1 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -24,8 +24,8 @@ const tk = require('timekeeper') const { EventEmitter } = require('events') const Path = require('path') -describe('CompileManager', function() { - beforeEach(function() { +describe('CompileManager', function () { + beforeEach(function () { this.CompileManager = SandboxedModule.require(modulePath, { requires: { './LatexRunner': (this.LatexRunner = {}), @@ -60,8 +60,8 @@ describe('CompileManager', function() { this.project_id = 'project-id-123' return (this.user_id = '1234') }) - describe('doCompileWithLock', function() { - beforeEach(function() { + describe('doCompileWithLock', function () { + beforeEach(function () { this.request = { resources: (this.resources = 'mock-resources'), project_id: this.project_id, @@ -77,33 +77,33 @@ describe('CompileManager', function() { runner((err, ...result) => callback(err, ...Array.from(result)))) }) - describe('when the project is not locked', function() { - beforeEach(function() { + describe('when the project is not locked', function () { + beforeEach(function () { return this.CompileManager.doCompileWithLock( this.request, this.callback ) }) - it('should ensure that the compile directory exists', function() { + it('should ensure that the compile directory exists', function () { return this.fse.ensureDir.calledWith(this.compileDir).should.equal(true) }) - it('should call doCompile with the request', function() { + it('should call doCompile with the request', function () { return this.CompileManager.doCompile .calledWith(this.request) .should.equal(true) }) - return it('should call the callback with the output files', function() { + return it('should call the callback with the output files', function () { return this.callback .calledWithExactly(null, this.output_files) .should.equal(true) }) }) - return describe('when the project is locked', function() { - beforeEach(function() { + return describe('when the project is locked', function () { + beforeEach(function () { this.error = new Error('locked') this.LockManager.runWithLock = (lockFile, runner, callback) => { return callback(this.error) @@ -114,22 +114,22 @@ describe('CompileManager', function() { ) }) - it('should ensure that the compile directory exists', function() { + it('should ensure that the compile directory exists', function () { return this.fse.ensureDir.calledWith(this.compileDir).should.equal(true) }) - it('should not call doCompile with the request', function() { + it('should not call doCompile with the request', function () { return this.CompileManager.doCompile.called.should.equal(false) }) - return it('should call the callback with the error', function() { + return it('should call the callback with the error', function () { return this.callback.calledWithExactly(this.error).should.equal(true) }) }) }) - describe('doCompile', function() { - beforeEach(function() { + describe('doCompile', function () { + beforeEach(function () { this.output_files = [ { path: 'output.log', @@ -180,18 +180,18 @@ describe('CompileManager', function() { return (this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false)) }) - describe('normally', function() { - beforeEach(function() { + describe('normally', function () { + beforeEach(function () { return this.CompileManager.doCompile(this.request, this.callback) }) - it('should write the resources to disk', function() { + it('should write the resources to disk', function () { return this.ResourceWriter.syncResourcesToDisk .calledWith(this.request, this.compileDir) .should.equal(true) }) - it('should run LaTeX', function() { + it('should run LaTeX', function () { return this.LatexRunner.runLatex .calledWith(`${this.project_id}-${this.user_id}`, { directory: this.compileDir, @@ -206,43 +206,43 @@ describe('CompileManager', function() { .should.equal(true) }) - it('should find the output files', function() { + it('should find the output files', function () { return this.OutputFileFinder.findOutputFiles .calledWith(this.resources, this.compileDir) .should.equal(true) }) - it('should return the output files', function() { + it('should return the output files', function () { return this.callback .calledWith(null, this.build_files) .should.equal(true) }) - return it('should not inject draft mode by default', function() { + return it('should not inject draft mode by default', function () { return this.DraftModeManager.injectDraftMode.called.should.equal(false) }) }) - describe('with draft mode', function() { - beforeEach(function() { + describe('with draft mode', function () { + beforeEach(function () { this.request.draft = true return this.CompileManager.doCompile(this.request, this.callback) }) - return it('should inject the draft mode header', function() { + return it('should inject the draft mode header', function () { return this.DraftModeManager.injectDraftMode .calledWith(this.compileDir + '/' + this.rootResourcePath) .should.equal(true) }) }) - describe('with a check option', function() { - beforeEach(function() { + describe('with a check option', function () { + beforeEach(function () { this.request.check = 'error' return this.CompileManager.doCompile(this.request, this.callback) }) - return it('should run chktex', function() { + return it('should run chktex', function () { return this.LatexRunner.runLatex .calledWith(`${this.project_id}-${this.user_id}`, { directory: this.compileDir, @@ -262,14 +262,14 @@ describe('CompileManager', function() { }) }) - return describe('with a knitr file and check options', function() { - beforeEach(function() { + return describe('with a knitr file and check options', function () { + beforeEach(function () { this.request.rootResourcePath = 'main.Rtex' this.request.check = 'error' return this.CompileManager.doCompile(this.request, this.callback) }) - return it('should not run chktex', function() { + return it('should not run chktex', function () { return this.LatexRunner.runLatex .calledWith(`${this.project_id}-${this.user_id}`, { directory: this.compileDir, @@ -286,9 +286,9 @@ describe('CompileManager', function() { }) }) - describe('clearProject', function() { - describe('succesfully', function() { - beforeEach(function() { + describe('clearProject', function () { + describe('succesfully', function () { + beforeEach(function () { this.Settings.compileDir = 'compiles' this.fs.lstat = sinon.stub().callsArgWith(1, null, { isDirectory() { @@ -308,7 +308,7 @@ describe('CompileManager', function() { return this.proc.emit('close', 0) }) - it('should remove the project directory', function() { + it('should remove the project directory', function () { return this.child_process.spawn .calledWith('rm', [ '-r', @@ -317,13 +317,13 @@ describe('CompileManager', function() { .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - return describe('with a non-success status code', function() { - beforeEach(function() { + return describe('with a non-success status code', function () { + beforeEach(function () { this.Settings.compileDir = 'compiles' this.fs.lstat = sinon.stub().callsArgWith(1, null, { isDirectory() { @@ -344,7 +344,7 @@ describe('CompileManager', function() { return this.proc.emit('close', 1) }) - it('should remove the project directory', function() { + it('should remove the project directory', function () { return this.child_process.spawn .calledWith('rm', [ '-r', @@ -353,7 +353,7 @@ describe('CompileManager', function() { .should.equal(true) }) - it('should call the callback with an error from the stderr', function() { + it('should call the callback with an error from the stderr', function () { this.callback.calledWithExactly(sinon.match(Error)).should.equal(true) this.callback.args[0][0].message.should.equal( @@ -363,8 +363,8 @@ describe('CompileManager', function() { }) }) - describe('syncing', function() { - beforeEach(function() { + describe('syncing', function () { + beforeEach(function () { this.page = 1 this.h = 42.23 this.v = 87.56 @@ -374,12 +374,12 @@ describe('CompileManager', function() { this.column = 3 this.file_name = 'main.tex' this.child_process.execFile = sinon.stub() - return (this.Settings.path.synctexBaseDir = project_id => + return (this.Settings.path.synctexBaseDir = (project_id) => `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`) }) - describe('syncFromCode', function() { - beforeEach(function() { + describe('syncFromCode', function () { + beforeEach(function () { this.fs.stat = sinon.stub().callsArgWith(1, null, { isFile() { return true @@ -399,7 +399,7 @@ describe('CompileManager', function() { ) }) - it('should execute the synctex binary', function() { + it('should execute the synctex binary', function () { const bin_path = Path.resolve(__dirname + '/../../../bin/synctex') const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf` const file_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}` @@ -422,7 +422,7 @@ describe('CompileManager', function() { .should.equal(true) }) - return it('should call the callback with the parsed output', function() { + return it('should call the callback with the parsed output', function () { return this.callback .calledWith(null, [ { @@ -437,8 +437,8 @@ describe('CompileManager', function() { }) }) - return describe('syncFromPdf', function() { - beforeEach(function() { + return describe('syncFromPdf', function () { + beforeEach(function () { this.fs.stat = sinon.stub().callsArgWith(1, null, { isFile() { return true @@ -458,7 +458,7 @@ describe('CompileManager', function() { ) }) - it('should execute the synctex binary', function() { + it('should execute the synctex binary', function () { const bin_path = Path.resolve(__dirname + '/../../../bin/synctex') const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf` return this.CommandRunner.run @@ -473,7 +473,7 @@ describe('CompileManager', function() { .should.equal(true) }) - return it('should call the callback with the parsed output', function() { + return it('should call the callback with the parsed output', function () { return this.callback .calledWith(null, [ { @@ -487,8 +487,8 @@ describe('CompileManager', function() { }) }) - return describe('wordcount', function() { - beforeEach(function() { + return describe('wordcount', function () { + beforeEach(function () { this.CommandRunner.run = sinon.stub().callsArg(7) this.fs.readFile = sinon .stub() @@ -514,7 +514,7 @@ describe('CompileManager', function() { ) }) - it('should run the texcount command', function() { + it('should run the texcount command', function () { this.directory = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` this.file_path = `$COMPILE_DIR/${this.file_name}` this.command = [ @@ -537,7 +537,7 @@ describe('CompileManager', function() { .should.equal(true) }) - return it('should call the callback with the parsed output', function() { + return it('should call the callback with the parsed output', function () { return this.callback .calledWith(null, { encode: 'ascii', diff --git a/test/unit/js/ContentTypeMapperTests.js b/test/unit/js/ContentTypeMapperTests.js index 41fc37e4..21173bc5 100644 --- a/test/unit/js/ContentTypeMapperTests.js +++ b/test/unit/js/ContentTypeMapperTests.js @@ -18,61 +18,61 @@ const modulePath = require('path').join( '../../../app/js/ContentTypeMapper' ) -describe('ContentTypeMapper', function() { - beforeEach(function() { +describe('ContentTypeMapper', function () { + beforeEach(function () { return (this.ContentTypeMapper = SandboxedModule.require(modulePath)) }) - return describe('map', function() { - it('should map .txt to text/plain', function() { + return describe('map', function () { + it('should map .txt to text/plain', function () { const content_type = this.ContentTypeMapper.map('example.txt') return content_type.should.equal('text/plain') }) - it('should map .csv to text/csv', function() { + it('should map .csv to text/csv', function () { const content_type = this.ContentTypeMapper.map('example.csv') return content_type.should.equal('text/csv') }) - it('should map .pdf to application/pdf', function() { + it('should map .pdf to application/pdf', function () { const content_type = this.ContentTypeMapper.map('example.pdf') return content_type.should.equal('application/pdf') }) - it('should fall back to octet-stream', function() { + it('should fall back to octet-stream', function () { const content_type = this.ContentTypeMapper.map('example.unknown') return content_type.should.equal('application/octet-stream') }) - describe('coercing web files to plain text', function() { - it('should map .js to plain text', function() { + describe('coercing web files to plain text', function () { + it('should map .js to plain text', function () { const content_type = this.ContentTypeMapper.map('example.js') return content_type.should.equal('text/plain') }) - it('should map .html to plain text', function() { + it('should map .html to plain text', function () { const content_type = this.ContentTypeMapper.map('example.html') return content_type.should.equal('text/plain') }) - return it('should map .css to plain text', function() { + return it('should map .css to plain text', function () { const content_type = this.ContentTypeMapper.map('example.css') return content_type.should.equal('text/plain') }) }) - return describe('image files', function() { - it('should map .png to image/png', function() { + return describe('image files', function () { + it('should map .png to image/png', function () { const content_type = this.ContentTypeMapper.map('example.png') return content_type.should.equal('image/png') }) - it('should map .jpeg to image/jpeg', function() { + it('should map .jpeg to image/jpeg', function () { const content_type = this.ContentTypeMapper.map('example.jpeg') return content_type.should.equal('image/jpeg') }) - return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function() { + return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function () { const content_type = this.ContentTypeMapper.map('example.svg') return content_type.should.equal('text/plain') }) diff --git a/test/unit/js/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js index bc13c5ad..19c289a1 100644 --- a/test/unit/js/DockerLockManagerTests.js +++ b/test/unit/js/DockerLockManagerTests.js @@ -17,8 +17,8 @@ const modulePath = require('path').join( '../../../app/js/DockerLockManager' ) -describe('LockManager', function() { - beforeEach(function() { +describe('LockManager', function () { + beforeEach(function () { return (this.LockManager = SandboxedModule.require(modulePath, { requires: { 'settings-sharelatex': (this.Settings = { clsi: { docker: {} } }), @@ -30,13 +30,13 @@ describe('LockManager', function() { })) }) - return describe('runWithLock', function() { - describe('with a single lock', function() { - beforeEach(function(done) { + return describe('runWithLock', function () { + describe('with a single lock', function () { + beforeEach(function (done) { this.callback = sinon.stub() return this.LockManager.runWithLock( 'lock-one', - releaseLock => + (releaseLock) => setTimeout(() => releaseLock(null, 'hello', 'world'), 100), (err, ...args) => { @@ -46,20 +46,20 @@ describe('LockManager', function() { ) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback .calledWith(null, 'hello', 'world') .should.equal(true) }) }) - describe('with two locks', function() { - beforeEach(function(done) { + describe('with two locks', function () { + beforeEach(function (done) { this.callback1 = sinon.stub() this.callback2 = sinon.stub() this.LockManager.runWithLock( 'lock-one', - releaseLock => + (releaseLock) => setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100), (err, ...args) => { @@ -68,7 +68,7 @@ describe('LockManager', function() { ) return this.LockManager.runWithLock( 'lock-two', - releaseLock => + (releaseLock) => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200), (err, ...args) => { @@ -78,29 +78,29 @@ describe('LockManager', function() { ) }) - it('should call the first callback', function() { + it('should call the first callback', function () { return this.callback1 .calledWith(null, 'hello', 'world', 'one') .should.equal(true) }) - return it('should call the second callback', function() { + return it('should call the second callback', function () { return this.callback2 .calledWith(null, 'hello', 'world', 'two') .should.equal(true) }) }) - return describe('with lock contention', function() { - describe('where the first lock is released quickly', function() { - beforeEach(function(done) { + return describe('with lock contention', function () { + describe('where the first lock is released quickly', function () { + beforeEach(function (done) { this.LockManager.MAX_LOCK_WAIT_TIME = 1000 this.LockManager.LOCK_TEST_INTERVAL = 100 this.callback1 = sinon.stub() this.callback2 = sinon.stub() this.LockManager.runWithLock( 'lock', - releaseLock => + (releaseLock) => setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100), (err, ...args) => { @@ -109,7 +109,7 @@ describe('LockManager', function() { ) return this.LockManager.runWithLock( 'lock', - releaseLock => + (releaseLock) => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200), (err, ...args) => { @@ -119,21 +119,21 @@ describe('LockManager', function() { ) }) - it('should call the first callback', function() { + it('should call the first callback', function () { return this.callback1 .calledWith(null, 'hello', 'world', 'one') .should.equal(true) }) - return it('should call the second callback', function() { + return it('should call the second callback', function () { return this.callback2 .calledWith(null, 'hello', 'world', 'two') .should.equal(true) }) }) - describe('where the first lock is held longer than the waiting time', function() { - beforeEach(function(done) { + describe('where the first lock is held longer than the waiting time', function () { + beforeEach(function (done) { let doneTwo this.LockManager.MAX_LOCK_HOLD_TIME = 10000 this.LockManager.MAX_LOCK_WAIT_TIME = 1000 @@ -141,7 +141,7 @@ describe('LockManager', function() { this.callback1 = sinon.stub() this.callback2 = sinon.stub() let doneOne = (doneTwo = false) - const finish = function(key) { + const finish = function (key) { if (key === 1) { doneOne = true } @@ -154,7 +154,7 @@ describe('LockManager', function() { } this.LockManager.runWithLock( 'lock', - releaseLock => + (releaseLock) => setTimeout( () => releaseLock(null, 'hello', 'world', 'one'), 1100 @@ -167,7 +167,7 @@ describe('LockManager', function() { ) return this.LockManager.runWithLock( 'lock', - releaseLock => + (releaseLock) => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100), (err, ...args) => { @@ -177,20 +177,20 @@ describe('LockManager', function() { ) }) - it('should call the first callback', function() { + it('should call the first callback', function () { return this.callback1 .calledWith(null, 'hello', 'world', 'one') .should.equal(true) }) - return it('should call the second callback with an error', function() { + return it('should call the second callback with an error', function () { const error = sinon.match.instanceOf(Error) return this.callback2.calledWith(error).should.equal(true) }) }) - return describe('where the first lock is held longer than the max holding time', function() { - beforeEach(function(done) { + return describe('where the first lock is held longer than the max holding time', function () { + beforeEach(function (done) { let doneTwo this.LockManager.MAX_LOCK_HOLD_TIME = 1000 this.LockManager.MAX_LOCK_WAIT_TIME = 2000 @@ -198,7 +198,7 @@ describe('LockManager', function() { this.callback1 = sinon.stub() this.callback2 = sinon.stub() let doneOne = (doneTwo = false) - const finish = function(key) { + const finish = function (key) { if (key === 1) { doneOne = true } @@ -211,7 +211,7 @@ describe('LockManager', function() { } this.LockManager.runWithLock( 'lock', - releaseLock => + (releaseLock) => setTimeout( () => releaseLock(null, 'hello', 'world', 'one'), 1500 @@ -224,7 +224,7 @@ describe('LockManager', function() { ) return this.LockManager.runWithLock( 'lock', - releaseLock => + (releaseLock) => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100), (err, ...args) => { @@ -234,13 +234,13 @@ describe('LockManager', function() { ) }) - it('should call the first callback', function() { + it('should call the first callback', function () { return this.callback1 .calledWith(null, 'hello', 'world', 'one') .should.equal(true) }) - return it('should call the second callback', function() { + return it('should call the second callback', function () { return this.callback2 .calledWith(null, 'hello', 'world', 'two') .should.equal(true) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 9c1731a8..553b20f3 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -23,8 +23,8 @@ const modulePath = require('path').join( ) const Path = require('path') -describe('DockerRunner', function() { - beforeEach(function() { +describe('DockerRunner', function () { + beforeEach(function () { let container, Docker, Timer this.container = container = {} this.DockerRunner = SandboxedModule.require(modulePath, { @@ -39,7 +39,7 @@ describe('DockerRunner', function() { info: sinon.stub(), warn: sinon.stub() }), - dockerode: (Docker = (function() { + dockerode: (Docker = (function () { Docker = class Docker { static initClass() { this.prototype.getContainer = sinon.stub().returns(container) @@ -90,12 +90,12 @@ describe('DockerRunner', function() { return (this.Settings.clsi.docker.env = { PATH: 'mock-path' }) }) - afterEach(function() { + afterEach(function () { this.DockerRunner.stopContainerMonitor() }) - describe('run', function() { - beforeEach(function(done) { + describe('run', function () { + beforeEach(function (done) { this.DockerRunner._getContainerOptions = sinon .stub() .returns((this.options = { mockoptions: 'foo' })) @@ -111,8 +111,8 @@ describe('DockerRunner', function() { return done() }) - describe('successfully', function() { - beforeEach(function(done) { + describe('successfully', function () { + beforeEach(function (done) { this.DockerRunner._runAndWaitForContainer = sinon .stub() .callsArgWith(3, null, (this.output = 'mock-output')) @@ -131,7 +131,7 @@ describe('DockerRunner', function() { ) }) - it('should generate the options for the container', function() { + it('should generate the options for the container', function () { return this.DockerRunner._getContainerOptions .calledWith( this.command_with_dir, @@ -142,25 +142,25 @@ describe('DockerRunner', function() { .should.equal(true) }) - it('should generate the fingerprint from the returned options', function() { + it('should generate the fingerprint from the returned options', function () { return this.DockerRunner._fingerprintContainer .calledWith(this.options) .should.equal(true) }) - it('should do the run', function() { + it('should do the run', function () { return this.DockerRunner._runAndWaitForContainer .calledWith(this.options, this.volumes, this.timeout) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.calledWith(null, this.output).should.equal(true) }) }) - describe('when path.sandboxedCompilesHostDir is set', function() { - beforeEach(function() { + describe('when path.sandboxedCompilesHostDir is set', function () { + beforeEach(function () { this.Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles' this.directory = '/var/lib/sharelatex/data/compiles/xyz' this.DockerRunner._runAndWaitForContainer = sinon @@ -178,7 +178,7 @@ describe('DockerRunner', function() { ) }) - it('should re-write the bind directory', function() { + it('should re-write the bind directory', function () { const volumes = this.DockerRunner._runAndWaitForContainer.lastCall .args[1] return expect(volumes).to.deep.equal({ @@ -186,13 +186,13 @@ describe('DockerRunner', function() { }) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.calledWith(null, this.output).should.equal(true) }) }) - describe('when the run throws an error', function() { - beforeEach(function() { + describe('when the run throws an error', function () { + beforeEach(function () { let firstTime = true this.output = 'mock-output' this.DockerRunner._runAndWaitForContainer = ( @@ -202,7 +202,7 @@ describe('DockerRunner', function() { callback ) => { if (callback == null) { - callback = function(error, output) {} + callback = function (error, output) {} } if (firstTime) { firstTime = false @@ -227,25 +227,25 @@ describe('DockerRunner', function() { ) }) - it('should do the run twice', function() { + it('should do the run twice', function () { return this.DockerRunner._runAndWaitForContainer.calledTwice.should.equal( true ) }) - it('should destroy the container in between', function() { + it('should destroy the container in between', function () { return this.DockerRunner.destroyContainer .calledWith(this.name, null) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.calledWith(null, this.output).should.equal(true) }) }) - describe('with no image', function() { - beforeEach(function() { + describe('with no image', function () { + beforeEach(function () { this.DockerRunner._runAndWaitForContainer = sinon .stub() .callsArgWith(3, null, (this.output = 'mock-output')) @@ -261,7 +261,7 @@ describe('DockerRunner', function() { ) }) - return it('should use the default image', function() { + return it('should use the default image', function () { return this.DockerRunner._getContainerOptions .calledWith( this.command_with_dir, @@ -273,8 +273,8 @@ describe('DockerRunner', function() { }) }) - describe('with image override', function() { - beforeEach(function() { + describe('with image override', function () { + beforeEach(function () { this.Settings.texliveImageNameOveride = 'overrideimage.com/something' this.DockerRunner._runAndWaitForContainer = sinon .stub() @@ -291,14 +291,14 @@ describe('DockerRunner', function() { ) }) - return it('should use the override and keep the tag', function() { + return it('should use the override and keep the tag', function () { const image = this.DockerRunner._getContainerOptions.args[0][1] return image.should.equal('overrideimage.com/something/image:2016.2') }) }) - describe('with image restriction', function() { - beforeEach(function() { + describe('with image restriction', function () { + beforeEach(function () { this.Settings.clsi.docker.allowedImages = [ 'repo/image:tag1', 'repo/image:tag2' @@ -308,8 +308,8 @@ describe('DockerRunner', function() { .callsArgWith(3, null, (this.output = 'mock-output')) }) - describe('with a valid image', function() { - beforeEach(function() { + describe('with a valid image', function () { + beforeEach(function () { this.DockerRunner.run( this.project_id, this.command, @@ -322,13 +322,13 @@ describe('DockerRunner', function() { ) }) - it('should setup the container', function() { + it('should setup the container', function () { this.DockerRunner._getContainerOptions.called.should.equal(true) }) }) - describe('with a invalid image', function() { - beforeEach(function() { + describe('with a invalid image', function () { + beforeEach(function () { this.DockerRunner.run( this.project_id, this.command, @@ -341,21 +341,21 @@ describe('DockerRunner', function() { ) }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { const err = new Error('image not allowed') this.callback.called.should.equal(true) this.callback.args[0][0].message.should.equal(err.message) }) - it('should not setup the container', function() { + it('should not setup the container', function () { this.DockerRunner._getContainerOptions.called.should.equal(false) }) }) }) }) - describe('run with _getOptions', function() { - beforeEach(function(done) { + describe('run with _getOptions', function () { + beforeEach(function (done) { // this.DockerRunner._getContainerOptions = sinon // .stub() // .returns((this.options = { mockoptions: 'foo' })) @@ -371,8 +371,8 @@ describe('DockerRunner', function() { return done() }) - describe('when a compile group config is set', function() { - beforeEach(function() { + describe('when a compile group config is set', function () { + beforeEach(function () { this.Settings.clsi.docker.compileGroupConfig = { 'compile-group': { 'HostConfig.newProperty': 'new-property' @@ -394,7 +394,7 @@ describe('DockerRunner', function() { ) }) - it('should set the docker options for the compile group', function() { + it('should set the docker options for the compile group', function () { const options = this.DockerRunner._runAndWaitForContainer.lastCall .args[0] return expect(options.HostConfig).to.deep.include({ @@ -406,14 +406,14 @@ describe('DockerRunner', function() { }) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.calledWith(null, this.output).should.equal(true) }) }) }) - describe('_runAndWaitForContainer', function() { - beforeEach(function() { + describe('_runAndWaitForContainer', function () { + beforeEach(function () { this.options = { mockoptions: 'foo', name: (this.name = 'mock-name') } this.DockerRunner.startContainer = ( options, @@ -436,25 +436,25 @@ describe('DockerRunner', function() { ) }) - it('should create/start the container', function() { + it('should create/start the container', function () { return this.DockerRunner.startContainer .calledWith(this.options, this.volumes) .should.equal(true) }) - it('should wait for the container to finish', function() { + it('should wait for the container to finish', function () { return this.DockerRunner.waitForContainer .calledWith(this.name, this.timeout) .should.equal(true) }) - return it('should call the callback with the output', function() { + return it('should call the callback with the output', function () { return this.callback.calledWith(null, this.output).should.equal(true) }) }) - describe('startContainer', function() { - beforeEach(function() { + describe('startContainer', function () { + beforeEach(function () { this.attachStreamHandler = sinon.stub() this.attachStreamHandler.cock = true this.options = { mockoptions: 'foo', name: 'mock-name' } @@ -470,8 +470,8 @@ describe('DockerRunner', function() { return sinon.spy(this.DockerRunner, 'attachToContainer') }) - describe('when the container exists', function() { - beforeEach(function() { + describe('when the container exists', function () { + beforeEach(function () { this.container.inspect = sinon.stub().callsArgWith(0) this.container.start = sinon.stub().yields() @@ -483,24 +483,24 @@ describe('DockerRunner', function() { ) }) - it('should start the container with the given name', function() { + it('should start the container with the given name', function () { this.getContainer.calledWith(this.options.name).should.equal(true) return this.container.start.called.should.equal(true) }) - it('should not try to create the container', function() { + it('should not try to create the container', function () { return this.createContainer.called.should.equal(false) }) - it('should attach to the container', function() { + it('should attach to the container', function () { return this.DockerRunner.attachToContainer.called.should.equal(true) }) - it('should call the callback', function() { + it('should call the callback', function () { return this.callback.called.should.equal(true) }) - return it('should attach before the container starts', function() { + return it('should attach before the container starts', function () { return sinon.assert.callOrder( this.DockerRunner.attachToContainer, this.container.start @@ -508,8 +508,8 @@ describe('DockerRunner', function() { }) }) - describe('when the container does not exist', function() { - beforeEach(function() { + describe('when the container does not exist', function () { + beforeEach(function () { const exists = false this.container.start = sinon.stub().yields() this.container.inspect = sinon @@ -523,20 +523,20 @@ describe('DockerRunner', function() { ) }) - it('should create the container', function() { + it('should create the container', function () { return this.createContainer.calledWith(this.options).should.equal(true) }) - it('should call the callback and stream handler', function() { + it('should call the callback and stream handler', function () { this.attachStreamHandler.called.should.equal(true) return this.callback.called.should.equal(true) }) - it('should attach to the container', function() { + it('should attach to the container', function () { return this.DockerRunner.attachToContainer.called.should.equal(true) }) - return it('should attach before the container starts', function() { + return it('should attach before the container starts', function () { return sinon.assert.callOrder( this.DockerRunner.attachToContainer, this.container.start @@ -544,8 +544,8 @@ describe('DockerRunner', function() { }) }) - describe('when the container is already running', function() { - beforeEach(function() { + describe('when the container is already running', function () { + beforeEach(function () { const error = new Error( `HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.` ) @@ -560,18 +560,18 @@ describe('DockerRunner', function() { ) }) - it('should not try to create the container', function() { + it('should not try to create the container', function () { return this.createContainer.called.should.equal(false) }) - return it('should call the callback and stream handler without an error', function() { + return it('should call the callback and stream handler without an error', function () { this.attachStreamHandler.called.should.equal(true) return this.callback.called.should.equal(true) }) }) - describe('when a volume does not exist', function() { - beforeEach(function() { + describe('when a volume does not exist', function () { + beforeEach(function () { this.fs.stat = sinon.stub().yields(new Error('no such path')) return this.DockerRunner.startContainer( this.options, @@ -581,17 +581,17 @@ describe('DockerRunner', function() { ) }) - it('should not try to create the container', function() { + it('should not try to create the container', function () { return this.createContainer.called.should.equal(false) }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) }) }) - describe('when a volume exists but is not a directory', function() { - beforeEach(function() { + describe('when a volume exists but is not a directory', function () { + beforeEach(function () { this.fs.stat = sinon.stub().yields(null, { isDirectory() { return false @@ -605,17 +605,17 @@ describe('DockerRunner', function() { ) }) - it('should not try to create the container', function() { + it('should not try to create the container', function () { return this.createContainer.called.should.equal(false) }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) }) }) - describe('when a volume does not exist, but sibling-containers are used', function() { - beforeEach(function() { + describe('when a volume does not exist, but sibling-containers are used', function () { + beforeEach(function () { this.fs.stat = sinon.stub().yields(new Error('no such path')) this.Settings.path.sandboxedCompilesHostDir = '/some/path' this.container.start = sinon.stub().yields() @@ -626,30 +626,30 @@ describe('DockerRunner', function() { ) }) - afterEach(function() { + afterEach(function () { return delete this.Settings.path.sandboxedCompilesHostDir }) - it('should start the container with the given name', function() { + it('should start the container with the given name', function () { this.getContainer.calledWith(this.options.name).should.equal(true) return this.container.start.called.should.equal(true) }) - it('should not try to create the container', function() { + it('should not try to create the container', function () { return this.createContainer.called.should.equal(false) }) - return it('should call the callback', function() { + return it('should call the callback', function () { this.callback.called.should.equal(true) return this.callback.calledWith(new Error()).should.equal(false) }) }) - return describe('when the container tries to be created, but already has been (race condition)', function() {}) + return describe('when the container tries to be created, but already has been (race condition)', function () {}) }) - describe('waitForContainer', function() { - beforeEach(function() { + describe('waitForContainer', function () { + beforeEach(function () { this.containerId = 'container-id' this.timeout = 5000 this.container.wait = sinon @@ -658,8 +658,8 @@ describe('DockerRunner', function() { return (this.container.kill = sinon.stub().yields()) }) - describe('when the container returns in time', function() { - beforeEach(function() { + describe('when the container returns in time', function () { + beforeEach(function () { return this.DockerRunner.waitForContainer( this.containerId, this.timeout, @@ -667,23 +667,23 @@ describe('DockerRunner', function() { ) }) - it('should wait for the container', function() { + it('should wait for the container', function () { this.getContainer.calledWith(this.containerId).should.equal(true) return this.container.wait.called.should.equal(true) }) - return it('should call the callback with the exit', function() { + return it('should call the callback with the exit', function () { return this.callback .calledWith(null, this.statusCode) .should.equal(true) }) }) - return describe('when the container does not return before the timeout', function() { - beforeEach(function(done) { - this.container.wait = function(callback) { + return describe('when the container does not return before the timeout', function () { + beforeEach(function (done) { + this.container.wait = function (callback) { if (callback == null) { - callback = function(error, exitCode) {} + callback = function (error, exitCode) {} } return setTimeout(() => callback(null, { StatusCode: 42 }), 100) } @@ -698,12 +698,12 @@ describe('DockerRunner', function() { ) }) - it('should call kill on the container', function() { + it('should call kill on the container', function () { this.getContainer.calledWith(this.containerId).should.equal(true) return this.container.kill.called.should.equal(true) }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) const errorObj = this.callback.args[0][0] @@ -713,8 +713,8 @@ describe('DockerRunner', function() { }) }) - describe('destroyOldContainers', function() { - beforeEach(function(done) { + describe('destroyOldContainers', function () { + beforeEach(function (done) { const oneHourInSeconds = 60 * 60 const oneHourInMilliseconds = oneHourInSeconds * 1000 const nowInSeconds = Date.now() / 1000 @@ -738,42 +738,42 @@ describe('DockerRunner', function() { this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds this.listContainers.callsArgWith(1, null, this.containers) this.DockerRunner.destroyContainer = sinon.stub().callsArg(3) - return this.DockerRunner.destroyOldContainers(error => { + return this.DockerRunner.destroyOldContainers((error) => { this.callback(error) return done() }) }) - it('should list all containers', function() { + it('should list all containers', function () { return this.listContainers.calledWith({ all: true }).should.equal(true) }) - it('should destroy old containers', function() { + it('should destroy old containers', function () { this.DockerRunner.destroyContainer.callCount.should.equal(1) return this.DockerRunner.destroyContainer .calledWith('project-old-container-name', 'old-container-id') .should.equal(true) }) - it('should not destroy new containers', function() { + it('should not destroy new containers', function () { return this.DockerRunner.destroyContainer .calledWith('project-new-container-name', 'new-container-id') .should.equal(false) }) - it('should not destroy non-project containers', function() { + it('should not destroy non-project containers', function () { return this.DockerRunner.destroyContainer .calledWith('totally-not-a-project-container', 'some-random-id') .should.equal(false) }) - return it('should callback the callback', function() { + return it('should callback the callback', function () { return this.callback.called.should.equal(true) }) }) - describe('_destroyContainer', function() { - beforeEach(function() { + describe('_destroyContainer', function () { + beforeEach(function () { this.containerId = 'some_id' this.fakeContainer = { remove: sinon.stub().callsArgWith(1, null) } return (this.Docker.prototype.getContainer = sinon @@ -781,11 +781,11 @@ describe('DockerRunner', function() { .returns(this.fakeContainer)) }) - it('should get the container', function(done) { + it('should get the container', function (done) { return this.DockerRunner._destroyContainer( this.containerId, false, - err => { + (err) => { this.Docker.prototype.getContainer.callCount.should.equal(1) this.Docker.prototype.getContainer .calledWith(this.containerId) @@ -795,11 +795,11 @@ describe('DockerRunner', function() { ) }) - it('should try to force-destroy the container when shouldForce=true', function(done) { + it('should try to force-destroy the container when shouldForce=true', function (done) { return this.DockerRunner._destroyContainer( this.containerId, true, - err => { + (err) => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove .calledWith({ force: true }) @@ -809,11 +809,11 @@ describe('DockerRunner', function() { ) }) - it('should not try to force-destroy the container when shouldForce=false', function(done) { + it('should not try to force-destroy the container when shouldForce=false', function (done) { return this.DockerRunner._destroyContainer( this.containerId, false, - err => { + (err) => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove .calledWith({ force: false }) @@ -823,19 +823,19 @@ describe('DockerRunner', function() { ) }) - it('should not produce an error', function(done) { + it('should not produce an error', function (done) { return this.DockerRunner._destroyContainer( this.containerId, false, - err => { + (err) => { expect(err).to.equal(null) return done() } ) }) - describe('when the container is already gone', function() { - beforeEach(function() { + describe('when the container is already gone', function () { + beforeEach(function () { this.fakeError = new Error('woops') this.fakeError.statusCode = 404 this.fakeContainer = { @@ -846,11 +846,11 @@ describe('DockerRunner', function() { .returns(this.fakeContainer)) }) - return it('should not produce an error', function(done) { + return it('should not produce an error', function (done) { return this.DockerRunner._destroyContainer( this.containerId, false, - err => { + (err) => { expect(err).to.equal(null) return done() } @@ -858,8 +858,8 @@ describe('DockerRunner', function() { }) }) - return describe('when container.destroy produces an error', function(done) { - beforeEach(function() { + return describe('when container.destroy produces an error', function (done) { + beforeEach(function () { this.fakeError = new Error('woops') this.fakeError.statusCode = 500 this.fakeContainer = { @@ -870,11 +870,11 @@ describe('DockerRunner', function() { .returns(this.fakeContainer)) }) - return it('should produce an error', function(done) { + return it('should produce an error', function (done) { return this.DockerRunner._destroyContainer( this.containerId, false, - err => { + (err) => { expect(err).to.not.equal(null) expect(err).to.equal(this.fakeError) return done() @@ -884,8 +884,8 @@ describe('DockerRunner', function() { }) }) - return describe('kill', function() { - beforeEach(function() { + return describe('kill', function () { + beforeEach(function () { this.containerId = 'some_id' this.fakeContainer = { kill: sinon.stub().callsArgWith(0, null) } return (this.Docker.prototype.getContainer = sinon @@ -893,8 +893,8 @@ describe('DockerRunner', function() { .returns(this.fakeContainer)) }) - it('should get the container', function(done) { - return this.DockerRunner.kill(this.containerId, err => { + it('should get the container', function (done) { + return this.DockerRunner.kill(this.containerId, (err) => { this.Docker.prototype.getContainer.callCount.should.equal(1) this.Docker.prototype.getContainer .calledWith(this.containerId) @@ -903,22 +903,22 @@ describe('DockerRunner', function() { }) }) - it('should try to force-destroy the container', function(done) { - return this.DockerRunner.kill(this.containerId, err => { + it('should try to force-destroy the container', function (done) { + return this.DockerRunner.kill(this.containerId, (err) => { this.fakeContainer.kill.callCount.should.equal(1) return done() }) }) - it('should not produce an error', function(done) { - return this.DockerRunner.kill(this.containerId, err => { + it('should not produce an error', function (done) { + return this.DockerRunner.kill(this.containerId, (err) => { expect(err).to.equal(undefined) return done() }) }) - describe('when the container is not actually running', function() { - beforeEach(function() { + describe('when the container is not actually running', function () { + beforeEach(function () { this.fakeError = new Error('woops') this.fakeError.statusCode = 500 this.fakeError.message = @@ -931,16 +931,16 @@ describe('DockerRunner', function() { .returns(this.fakeContainer)) }) - return it('should not produce an error', function(done) { - return this.DockerRunner.kill(this.containerId, err => { + return it('should not produce an error', function (done) { + return this.DockerRunner.kill(this.containerId, (err) => { expect(err).to.equal(undefined) return done() }) }) }) - return describe('when container.kill produces a legitimate error', function(done) { - beforeEach(function() { + return describe('when container.kill produces a legitimate error', function (done) { + beforeEach(function () { this.fakeError = new Error('woops') this.fakeError.statusCode = 500 this.fakeError.message = 'Totally legitimate reason to throw an error' @@ -952,8 +952,8 @@ describe('DockerRunner', function() { .returns(this.fakeContainer)) }) - return it('should produce an error', function(done) { - return this.DockerRunner.kill(this.containerId, err => { + return it('should produce an error', function (done) { + return this.DockerRunner.kill(this.containerId, (err) => { expect(err).to.not.equal(undefined) expect(err).to.equal(this.fakeError) return done() diff --git a/test/unit/js/DraftModeManagerTests.js b/test/unit/js/DraftModeManagerTests.js index 2c30b404..937cde90 100644 --- a/test/unit/js/DraftModeManagerTests.js +++ b/test/unit/js/DraftModeManagerTests.js @@ -16,8 +16,8 @@ const modulePath = require('path').join( '../../../app/js/DraftModeManager' ) -describe('DraftModeManager', function() { - beforeEach(function() { +describe('DraftModeManager', function () { + beforeEach(function () { return (this.DraftModeManager = SandboxedModule.require(modulePath, { requires: { fs: (this.fs = {}), @@ -26,8 +26,8 @@ describe('DraftModeManager', function() { })) }) - describe('_injectDraftOption', function() { - it('should add draft option into documentclass with existing options', function() { + describe('_injectDraftOption', function () { + it('should add draft option into documentclass with existing options', function () { return this.DraftModeManager._injectDraftOption(`\ \\documentclass[a4paper,foo=bar]{article}\ `).should.equal(`\ @@ -35,7 +35,7 @@ describe('DraftModeManager', function() { `) }) - return it('should add draft option into documentclass with no options', function() { + return it('should add draft option into documentclass with no options', function () { return this.DraftModeManager._injectDraftOption(`\ \\documentclass{article}\ `).should.equal(`\ @@ -44,8 +44,8 @@ describe('DraftModeManager', function() { }) }) - return describe('injectDraftMode', function() { - beforeEach(function() { + return describe('injectDraftMode', function () { + beforeEach(function () { this.filename = '/mock/filename.tex' this.callback = sinon.stub() const content = `\ @@ -59,13 +59,13 @@ Hello world return this.DraftModeManager.injectDraftMode(this.filename, this.callback) }) - it('should read the file', function() { + it('should read the file', function () { return this.fs.readFile .calledWith(this.filename, 'utf8') .should.equal(true) }) - it('should write the modified file', function() { + it('should write the modified file', function () { return this.fs.writeFile .calledWith( this.filename, @@ -79,7 +79,7 @@ Hello world .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index f480bc82..9cd0d0ac 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -18,8 +18,8 @@ const modulePath = require('path').join( ) const Path = require('path') -describe('LatexRunner', function() { - beforeEach(function() { +describe('LatexRunner', function () { + beforeEach(function () { let Timer this.LatexRunner = SandboxedModule.require(modulePath, { requires: { @@ -54,16 +54,16 @@ describe('LatexRunner', function() { return (this.env = { foo: '123' }) }) - return describe('runLatex', function() { - beforeEach(function() { + return describe('runLatex', function () { + beforeEach(function () { return (this.CommandRunner.run = sinon.stub().callsArgWith(7, null, { stdout: 'this is stdout', stderr: 'this is stderr' })) }) - describe('normally', function() { - beforeEach(function() { + describe('normally', function () { + beforeEach(function () { return this.LatexRunner.runLatex( this.project_id, { @@ -79,7 +79,7 @@ describe('LatexRunner', function() { ) }) - it('should run the latex command', function() { + it('should run the latex command', function () { return this.CommandRunner.run .calledWith( this.project_id, @@ -93,7 +93,7 @@ describe('LatexRunner', function() { .should.equal(true) }) - it('should record the stdout and stderr', function() { + it('should record the stdout and stderr', function () { this.fs.writeFile .calledWith(this.directory + '/' + 'output.stdout', 'this is stdout') .should.equal(true) @@ -103,8 +103,8 @@ describe('LatexRunner', function() { }) }) - describe('with an .Rtex main file', function() { - beforeEach(function() { + describe('with an .Rtex main file', function () { + beforeEach(function () { return this.LatexRunner.runLatex( this.project_id, { @@ -118,15 +118,15 @@ describe('LatexRunner', function() { ) }) - return it('should run the latex command on the equivalent .tex file', function() { + return it('should run the latex command on the equivalent .tex file', function () { const command = this.CommandRunner.run.args[0][1] const mainFile = command.slice(-1)[0] return mainFile.should.equal('$COMPILE_DIR/main-file.tex') }) }) - return describe('with a flags option', function() { - beforeEach(function() { + return describe('with a flags option', function () { + beforeEach(function () { return this.LatexRunner.runLatex( this.project_id, { @@ -141,10 +141,10 @@ describe('LatexRunner', function() { ) }) - return it('should include the flags in the command', function() { + return it('should include the flags in the command', function () { const command = this.CommandRunner.run.args[0][1] const flags = command.filter( - arg => arg === '-file-line-error' || arg === '-halt-on-error' + (arg) => arg === '-file-line-error' || arg === '-halt-on-error' ) flags.length.should.equal(2) flags[0].should.equal('-file-line-error') diff --git a/test/unit/js/LockManagerTests.js b/test/unit/js/LockManagerTests.js index cb8ab9b7..16a43ade 100644 --- a/test/unit/js/LockManagerTests.js +++ b/test/unit/js/LockManagerTests.js @@ -19,8 +19,8 @@ const modulePath = require('path').join( const Path = require('path') const Errors = require('../../../app/js/Errors') -describe('DockerLockManager', function() { - beforeEach(function() { +describe('DockerLockManager', function () { + beforeEach(function () { this.LockManager = SandboxedModule.require(modulePath, { requires: { 'settings-sharelatex': {}, @@ -39,14 +39,14 @@ describe('DockerLockManager', function() { return (this.lockFile = '/local/compile/directory/.project-lock') }) - return describe('runWithLock', function() { - beforeEach(function() { + return describe('runWithLock', function () { + beforeEach(function () { this.runner = sinon.stub().callsArgWith(0, null, 'foo', 'bar') return (this.callback = sinon.stub()) }) - describe('normally', function() { - beforeEach(function() { + describe('normally', function () { + beforeEach(function () { this.Lockfile.lock = sinon.stub().callsArgWith(2, null) this.Lockfile.unlock = sinon.stub().callsArgWith(1, null) return this.LockManager.runWithLock( @@ -56,19 +56,19 @@ describe('DockerLockManager', function() { ) }) - it('should run the compile', function() { + it('should run the compile', function () { return this.runner.calledWith().should.equal(true) }) - return it('should call the callback with the response from the compile', function() { + return it('should call the callback with the response from the compile', function () { return this.callback .calledWithExactly(null, 'foo', 'bar') .should.equal(true) }) }) - return describe('when the project is locked', function() { - beforeEach(function() { + return describe('when the project is locked', function () { + beforeEach(function () { this.error = new Error() this.error.code = 'EEXIST' this.Lockfile.lock = sinon.stub().callsArgWith(2, this.error) @@ -80,11 +80,11 @@ describe('DockerLockManager', function() { ) }) - it('should not run the compile', function() { + it('should not run the compile', function () { return this.runner.called.should.equal(false) }) - it('should return an error', function() { + it('should return an error', function () { this.callback .calledWithExactly(sinon.match(Errors.AlreadyCompilingError)) .should.equal(true) diff --git a/test/unit/js/OutputFileFinderTests.js b/test/unit/js/OutputFileFinderTests.js index ee591b40..4afa25e8 100644 --- a/test/unit/js/OutputFileFinderTests.js +++ b/test/unit/js/OutputFileFinderTests.js @@ -21,8 +21,8 @@ const path = require('path') const { expect } = require('chai') const { EventEmitter } = require('events') -describe('OutputFileFinder', function() { - beforeEach(function() { +describe('OutputFileFinder', function () { + beforeEach(function () { this.OutputFileFinder = SandboxedModule.require(modulePath, { requires: { fs: (this.fs = {}), @@ -34,8 +34,8 @@ describe('OutputFileFinder', function() { return (this.callback = sinon.stub()) }) - describe('findOutputFiles', function() { - beforeEach(function() { + describe('findOutputFiles', function () { + beforeEach(function () { this.resource_path = 'resource/path.tex' this.output_paths = ['output.pdf', 'extra/file.tex'] this.all_paths = this.output_paths.concat([this.resource_path]) @@ -52,7 +52,7 @@ describe('OutputFileFinder', function() { ) }) - return it('should only return the output files, not directories or resource paths', function() { + return it('should only return the output files, not directories or resource paths', function () { return expect(this.outputFiles).to.deep.equal([ { path: 'output.pdf', @@ -66,8 +66,8 @@ describe('OutputFileFinder', function() { }) }) - return describe('_getAllFiles', function() { - beforeEach(function() { + return describe('_getAllFiles', function () { + beforeEach(function () { this.proc = new EventEmitter() this.proc.stdout = new EventEmitter() this.proc.stdout.setEncoding = sinon.stub().returns(this.proc.stdout) @@ -76,8 +76,8 @@ describe('OutputFileFinder', function() { return this.OutputFileFinder._getAllFiles(this.directory, this.callback) }) - describe('successfully', function() { - beforeEach(function() { + describe('successfully', function () { + beforeEach(function () { this.proc.stdout.emit( 'data', ['/base/dir/main.tex', '/base/dir/chapters/chapter1.tex'].join('\n') + @@ -86,19 +86,19 @@ describe('OutputFileFinder', function() { return this.proc.emit('close', 0) }) - return it('should call the callback with the relative file paths', function() { + return it('should call the callback with the relative file paths', function () { return this.callback .calledWith(null, ['main.tex', 'chapters/chapter1.tex']) .should.equal(true) }) }) - return describe("when the directory doesn't exist", function() { - beforeEach(function() { + return describe("when the directory doesn't exist", function () { + beforeEach(function () { return this.proc.emit('close', 1) }) - return it('should call the callback with a blank array', function() { + return it('should call the callback with a blank array', function () { return this.callback.calledWith(null, []).should.equal(true) }) }) diff --git a/test/unit/js/OutputFileOptimiserTests.js b/test/unit/js/OutputFileOptimiserTests.js index 66904414..e7f7b8b0 100644 --- a/test/unit/js/OutputFileOptimiserTests.js +++ b/test/unit/js/OutputFileOptimiserTests.js @@ -21,8 +21,8 @@ const path = require('path') const { expect } = require('chai') const { EventEmitter } = require('events') -describe('OutputFileOptimiser', function() { - beforeEach(function() { +describe('OutputFileOptimiser', function () { + beforeEach(function () { this.OutputFileOptimiser = SandboxedModule.require(modulePath, { requires: { fs: (this.fs = {}), @@ -37,14 +37,14 @@ describe('OutputFileOptimiser', function() { return (this.callback = sinon.stub()) }) - describe('optimiseFile', function() { - beforeEach(function() { + describe('optimiseFile', function () { + beforeEach(function () { this.src = './output.pdf' return (this.dst = './output.pdf') }) - describe('when the file is not a pdf file', function() { - beforeEach(function(done) { + describe('when the file is not a pdf file', function () { + beforeEach(function (done) { this.src = './output.log' this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon .stub() @@ -55,21 +55,21 @@ describe('OutputFileOptimiser', function() { return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done) }) - it('should not check if the file is optimised', function() { + it('should not check if the file is optimised', function () { return this.OutputFileOptimiser.checkIfPDFIsOptimised .calledWith(this.src) .should.equal(false) }) - return it('should not optimise the file', function() { + return it('should not optimise the file', function () { return this.OutputFileOptimiser.optimisePDF .calledWith(this.src, this.dst) .should.equal(false) }) }) - describe('when the pdf file is not optimised', function() { - beforeEach(function(done) { + describe('when the pdf file is not optimised', function () { + beforeEach(function (done) { this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon .stub() .callsArgWith(1, null, false) @@ -79,21 +79,21 @@ describe('OutputFileOptimiser', function() { return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done) }) - it('should check if the pdf is optimised', function() { + it('should check if the pdf is optimised', function () { return this.OutputFileOptimiser.checkIfPDFIsOptimised .calledWith(this.src) .should.equal(true) }) - return it('should optimise the pdf', function() { + return it('should optimise the pdf', function () { return this.OutputFileOptimiser.optimisePDF .calledWith(this.src, this.dst) .should.equal(true) }) }) - return describe('when the pdf file is optimised', function() { - beforeEach(function(done) { + return describe('when the pdf file is optimised', function () { + beforeEach(function (done) { this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon .stub() .callsArgWith(1, null, true) @@ -103,13 +103,13 @@ describe('OutputFileOptimiser', function() { return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done) }) - it('should check if the pdf is optimised', function() { + it('should check if the pdf is optimised', function () { return this.OutputFileOptimiser.checkIfPDFIsOptimised .calledWith(this.src) .should.equal(true) }) - return it('should not optimise the pdf', function() { + return it('should not optimise the pdf', function () { return this.OutputFileOptimiser.optimisePDF .calledWith(this.src, this.dst) .should.equal(false) @@ -117,8 +117,8 @@ describe('OutputFileOptimiser', function() { }) }) - return describe('checkIfPDFISOptimised', function() { - beforeEach(function() { + return describe('checkIfPDFISOptimised', function () { + beforeEach(function () { this.callback = sinon.stub() this.fd = 1234 this.fs.open = sinon.stub().yields(null, this.fd) @@ -126,18 +126,15 @@ describe('OutputFileOptimiser', function() { .stub() .withArgs(this.fd) .yields(null, 100, Buffer.from('hello /Linearized 1')) - this.fs.close = sinon - .stub() - .withArgs(this.fd) - .yields(null) + this.fs.close = sinon.stub().withArgs(this.fd).yields(null) return this.OutputFileOptimiser.checkIfPDFIsOptimised( this.src, this.callback ) }) - describe('for a linearised file', function() { - beforeEach(function() { + describe('for a linearised file', function () { + beforeEach(function () { this.fs.read = sinon .stub() .withArgs(this.fd) @@ -148,25 +145,25 @@ describe('OutputFileOptimiser', function() { ) }) - it('should open the file', function() { + it('should open the file', function () { return this.fs.open.calledWith(this.src, 'r').should.equal(true) }) - it('should read the header', function() { + it('should read the header', function () { return this.fs.read.calledWith(this.fd).should.equal(true) }) - it('should close the file', function() { + it('should close the file', function () { return this.fs.close.calledWith(this.fd).should.equal(true) }) - return it('should call the callback with a true result', function() { + return it('should call the callback with a true result', function () { return this.callback.calledWith(null, true).should.equal(true) }) }) - return describe('for an unlinearised file', function() { - beforeEach(function() { + return describe('for an unlinearised file', function () { + beforeEach(function () { this.fs.read = sinon .stub() .withArgs(this.fd) @@ -177,19 +174,19 @@ describe('OutputFileOptimiser', function() { ) }) - it('should open the file', function() { + it('should open the file', function () { return this.fs.open.calledWith(this.src, 'r').should.equal(true) }) - it('should read the header', function() { + it('should read the header', function () { return this.fs.read.calledWith(this.fd).should.equal(true) }) - it('should close the file', function() { + it('should close the file', function () { return this.fs.close.calledWith(this.fd).should.equal(true) }) - return it('should call the callback with a false result', function() { + return it('should call the callback with a false result', function () { return this.callback.calledWith(null, false).should.equal(true) }) }) diff --git a/test/unit/js/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js index 1a12cfff..5e4368fd 100644 --- a/test/unit/js/ProjectPersistenceManagerTests.js +++ b/test/unit/js/ProjectPersistenceManagerTests.js @@ -21,8 +21,8 @@ const modulePath = require('path').join( ) const tk = require('timekeeper') -describe('ProjectPersistenceManager', function() { - beforeEach(function() { +describe('ProjectPersistenceManager', function () { + beforeEach(function () { this.ProjectPersistenceManager = SandboxedModule.require(modulePath, { requires: { './UrlCache': (this.UrlCache = {}), @@ -44,8 +44,8 @@ describe('ProjectPersistenceManager', function() { return (this.user_id = '1234') }) - describe('refreshExpiryTimeout', function() { - it('should leave expiry alone if plenty of disk', function(done) { + describe('refreshExpiryTimeout', function () { + it('should leave expiry alone if plenty of disk', function (done) { this.diskusage.check.callsArgWith(1, null, { available: 40, total: 100 @@ -59,7 +59,7 @@ describe('ProjectPersistenceManager', function() { }) }) - it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function(done) { + it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function (done) { this.diskusage.check.callsArgWith(1, null, { available: 5, total: 100 @@ -71,7 +71,7 @@ describe('ProjectPersistenceManager', function() { }) }) - it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function(done) { + it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function (done) { this.diskusage.check.callsArgWith(1, null, { available: 5, total: 100 @@ -83,7 +83,7 @@ describe('ProjectPersistenceManager', function() { }) }) - it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', function(done) { + it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', function (done) { this.diskusage.check.callsArgWith(1, 'Error', { available: 5, total: 100 @@ -95,8 +95,8 @@ describe('ProjectPersistenceManager', function() { }) }) - describe('clearExpiredProjects', function() { - beforeEach(function() { + describe('clearExpiredProjects', function () { + beforeEach(function () { this.project_ids = ['project-id-1', 'project-id-2'] this.ProjectPersistenceManager._findExpiredProjectIds = sinon .stub() @@ -108,21 +108,21 @@ describe('ProjectPersistenceManager', function() { return this.ProjectPersistenceManager.clearExpiredProjects(this.callback) }) - it('should clear each expired project', function() { - return Array.from(this.project_ids).map(project_id => + it('should clear each expired project', function () { + return Array.from(this.project_ids).map((project_id) => this.ProjectPersistenceManager.clearProjectFromCache .calledWith(project_id) .should.equal(true) ) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - return describe('clearProject', function() { - beforeEach(function() { + return describe('clearProject', function () { + beforeEach(function () { this.ProjectPersistenceManager._clearProjectFromDatabase = sinon .stub() .callsArg(1) @@ -135,25 +135,25 @@ describe('ProjectPersistenceManager', function() { ) }) - it('should clear the project from the database', function() { + it('should clear the project from the database', function () { return this.ProjectPersistenceManager._clearProjectFromDatabase .calledWith(this.project_id) .should.equal(true) }) - it('should clear all the cached Urls for the project', function() { + it('should clear all the cached Urls for the project', function () { return this.UrlCache.clearProject .calledWith(this.project_id) .should.equal(true) }) - it('should clear the project compile folder', function() { + it('should clear the project compile folder', function () { return this.CompileManager.clearProject .calledWith(this.project_id, this.user_id) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) diff --git a/test/unit/js/RequestParserTests.js b/test/unit/js/RequestParserTests.js index 25b6b293..6e6c9225 100644 --- a/test/unit/js/RequestParserTests.js +++ b/test/unit/js/RequestParserTests.js @@ -19,8 +19,8 @@ const modulePath = require('path').join( ) const tk = require('timekeeper') -describe('RequestParser', function() { - beforeEach(function() { +describe('RequestParser', function () { + beforeEach(function () { tk.freeze() this.callback = sinon.stub() this.validResource = { @@ -46,41 +46,41 @@ describe('RequestParser', function() { })) }) - afterEach(function() { + afterEach(function () { return tk.reset() }) - describe('without a top level object', function() { - beforeEach(function() { + describe('without a top level object', function () { + beforeEach(function () { return this.RequestParser.parse([], this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('top level object should have a compile attribute') .should.equal(true) }) }) - describe('without a compile attribute', function() { - beforeEach(function() { + describe('without a compile attribute', function () { + beforeEach(function () { return this.RequestParser.parse({}, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('top level object should have a compile attribute') .should.equal(true) }) }) - describe('without a valid compiler', function() { - beforeEach(function() { + describe('without a valid compiler', function () { + beforeEach(function () { this.validRequest.compile.options.compiler = 'not-a-compiler' return this.RequestParser.parse(this.validRequest, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith( 'compiler attribute should be one of: pdflatex, latex, xelatex, lualatex' @@ -89,33 +89,33 @@ describe('RequestParser', function() { }) }) - describe('without a compiler specified', function() { - beforeEach(function() { + describe('without a compiler specified', function () { + beforeEach(function () { delete this.validRequest.compile.options.compiler return this.RequestParser.parse(this.validRequest, (error, data) => { this.data = data }) }) - return it('should set the compiler to pdflatex by default', function() { + return it('should set the compiler to pdflatex by default', function () { return this.data.compiler.should.equal('pdflatex') }) }) - describe('with imageName set', function() { - beforeEach(function() { + describe('with imageName set', function () { + beforeEach(function () { return this.RequestParser.parse(this.validRequest, (error, data) => { this.data = data }) }) - return it('should set the imageName', function() { + return it('should set the imageName', function () { return this.data.imageName.should.equal('basicImageName/here:2017-1') }) }) - describe('when image restrictions are present', function() { - beforeEach(function() { + describe('when image restrictions are present', function () { + beforeEach(function () { this.settings.clsi = { docker: {} } this.settings.clsi.docker.allowedImages = [ 'repo/name:tag1', @@ -123,8 +123,8 @@ describe('RequestParser', function() { ] }) - describe('with imageName set to something invalid', function() { - beforeEach(function() { + describe('with imageName set to something invalid', function () { + beforeEach(function () { const request = this.validRequest request.compile.options.imageName = 'something/different:latest' this.RequestParser.parse(request, (error, data) => { @@ -133,15 +133,15 @@ describe('RequestParser', function() { }) }) - it('should throw an error for imageName', function() { + it('should throw an error for imageName', function () { expect(String(this.error)).to.include( 'imageName attribute should be one of' ) }) }) - describe('with imageName set to something valid', function() { - beforeEach(function() { + describe('with imageName set to something valid', function () { + beforeEach(function () { const request = this.validRequest request.compile.options.imageName = 'repo/name:tag1' this.RequestParser.parse(request, (error, data) => { @@ -150,54 +150,54 @@ describe('RequestParser', function() { }) }) - it('should set the imageName', function() { + it('should set the imageName', function () { this.data.imageName.should.equal('repo/name:tag1') }) }) }) - describe('with flags set', function() { - beforeEach(function() { + describe('with flags set', function () { + beforeEach(function () { this.validRequest.compile.options.flags = ['-file-line-error'] return this.RequestParser.parse(this.validRequest, (error, data) => { this.data = data }) }) - return it('should set the flags attribute', function() { + return it('should set the flags attribute', function () { return expect(this.data.flags).to.deep.equal(['-file-line-error']) }) }) - describe('with flags not specified', function() { - beforeEach(function() { + describe('with flags not specified', function () { + beforeEach(function () { return this.RequestParser.parse(this.validRequest, (error, data) => { this.data = data }) }) - return it('it should have an empty flags list', function() { + return it('it should have an empty flags list', function () { return expect(this.data.flags).to.deep.equal([]) }) }) - describe('without a timeout specified', function() { - beforeEach(function() { + describe('without a timeout specified', function () { + beforeEach(function () { delete this.validRequest.compile.options.timeout return this.RequestParser.parse(this.validRequest, (error, data) => { this.data = data }) }) - return it('should set the timeout to MAX_TIMEOUT', function() { + return it('should set the timeout to MAX_TIMEOUT', function () { return this.data.timeout.should.equal( this.RequestParser.MAX_TIMEOUT * 1000 ) }) }) - describe('with a timeout larger than the maximum', function() { - beforeEach(function() { + describe('with a timeout larger than the maximum', function () { + beforeEach(function () { this.validRequest.compile.options.timeout = this.RequestParser.MAX_TIMEOUT + 1 return this.RequestParser.parse(this.validRequest, (error, data) => { @@ -205,62 +205,62 @@ describe('RequestParser', function() { }) }) - return it('should set the timeout to MAX_TIMEOUT', function() { + return it('should set the timeout to MAX_TIMEOUT', function () { return this.data.timeout.should.equal( this.RequestParser.MAX_TIMEOUT * 1000 ) }) }) - describe('with a timeout', function() { - beforeEach(function() { + describe('with a timeout', function () { + beforeEach(function () { return this.RequestParser.parse(this.validRequest, (error, data) => { this.data = data }) }) - return it('should set the timeout (in milliseconds)', function() { + return it('should set the timeout (in milliseconds)', function () { return this.data.timeout.should.equal( this.validRequest.compile.options.timeout * 1000 ) }) }) - describe('with a resource without a path', function() { - beforeEach(function() { + describe('with a resource without a path', function () { + beforeEach(function () { delete this.validResource.path this.validRequest.compile.resources.push(this.validResource) return this.RequestParser.parse(this.validRequest, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('all resources should have a path attribute') .should.equal(true) }) }) - describe('with a resource with a path', function() { - beforeEach(function() { + describe('with a resource with a path', function () { + beforeEach(function () { this.validResource.path = this.path = 'test.tex' this.validRequest.compile.resources.push(this.validResource) this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it('should return the path in the parsed response', function() { + return it('should return the path in the parsed response', function () { return this.data.resources[0].path.should.equal(this.path) }) }) - describe('with a resource with a malformed modified date', function() { - beforeEach(function() { + describe('with a resource with a malformed modified date', function () { + beforeEach(function () { this.validResource.modified = 'not-a-date' this.validRequest.compile.resources.push(this.validResource) return this.RequestParser.parse(this.validRequest, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith( 'resource modified date could not be understood: ' + @@ -270,8 +270,8 @@ describe('RequestParser', function() { }) }) - describe('with a resource with a valid date', function() { - beforeEach(function() { + describe('with a resource with a valid date', function () { + beforeEach(function () { this.date = '12:00 01/02/03' this.validResource.modified = this.date this.validRequest.compile.resources.push(this.validResource) @@ -279,7 +279,7 @@ describe('RequestParser', function() { return (this.data = this.callback.args[0][1]) }) - return it('should return the date as a Javascript Date object', function() { + return it('should return the date as a Javascript Date object', function () { ;(this.data.resources[0].modified instanceof Date).should.equal(true) return this.data.resources[0].modified .getTime() @@ -287,15 +287,15 @@ describe('RequestParser', function() { }) }) - describe('with a resource without either a content or URL attribute', function() { - beforeEach(function() { + describe('with a resource without either a content or URL attribute', function () { + beforeEach(function () { delete this.validResource.url delete this.validResource.content this.validRequest.compile.resources.push(this.validResource) return this.RequestParser.parse(this.validRequest, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith( 'all resources should have either a url or content attribute' @@ -304,99 +304,99 @@ describe('RequestParser', function() { }) }) - describe('with a resource where the content is not a string', function() { - beforeEach(function() { + describe('with a resource where the content is not a string', function () { + beforeEach(function () { this.validResource.content = [] this.validRequest.compile.resources.push(this.validResource) return this.RequestParser.parse(this.validRequest, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('content attribute should be a string') .should.equal(true) }) }) - describe('with a resource where the url is not a string', function() { - beforeEach(function() { + describe('with a resource where the url is not a string', function () { + beforeEach(function () { this.validResource.url = [] this.validRequest.compile.resources.push(this.validResource) return this.RequestParser.parse(this.validRequest, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('url attribute should be a string') .should.equal(true) }) }) - describe('with a resource with a url', function() { - beforeEach(function() { + describe('with a resource with a url', function () { + beforeEach(function () { this.validResource.url = this.url = 'www.example.com' this.validRequest.compile.resources.push(this.validResource) this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it('should return the url in the parsed response', function() { + return it('should return the url in the parsed response', function () { return this.data.resources[0].url.should.equal(this.url) }) }) - describe('with a resource with a content attribute', function() { - beforeEach(function() { + describe('with a resource with a content attribute', function () { + beforeEach(function () { this.validResource.content = this.content = 'Hello world' this.validRequest.compile.resources.push(this.validResource) this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it('should return the content in the parsed response', function() { + return it('should return the content in the parsed response', function () { return this.data.resources[0].content.should.equal(this.content) }) }) - describe('without a root resource path', function() { - beforeEach(function() { + describe('without a root resource path', function () { + beforeEach(function () { delete this.validRequest.compile.rootResourcePath this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it("should set the root resource path to 'main.tex' by default", function() { + return it("should set the root resource path to 'main.tex' by default", function () { return this.data.rootResourcePath.should.equal('main.tex') }) }) - describe('with a root resource path', function() { - beforeEach(function() { + describe('with a root resource path', function () { + beforeEach(function () { this.validRequest.compile.rootResourcePath = this.path = 'test.tex' this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it('should return the root resource path in the parsed response', function() { + return it('should return the root resource path in the parsed response', function () { return this.data.rootResourcePath.should.equal(this.path) }) }) - describe('with a root resource path that is not a string', function() { - beforeEach(function() { + describe('with a root resource path that is not a string', function () { + beforeEach(function () { this.validRequest.compile.rootResourcePath = [] return this.RequestParser.parse(this.validRequest, this.callback) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('rootResourcePath attribute should be a string') .should.equal(true) }) }) - describe('with a root resource path that needs escaping', function() { - beforeEach(function() { + describe('with a root resource path that needs escaping', function () { + beforeEach(function () { this.badPath = '`rm -rf foo`.tex' this.goodPath = 'rm -rf foo.tex' this.validRequest.compile.rootResourcePath = this.badPath @@ -409,51 +409,51 @@ describe('RequestParser', function() { return (this.data = this.callback.args[0][1]) }) - it('should return the escaped resource', function() { + it('should return the escaped resource', function () { return this.data.rootResourcePath.should.equal(this.goodPath) }) - return it('should also escape the resource path', function() { + return it('should also escape the resource path', function () { return this.data.resources[0].path.should.equal(this.goodPath) }) }) - describe('with a root resource path that has a relative path', function() { - beforeEach(function() { + describe('with a root resource path that has a relative path', function () { + beforeEach(function () { this.validRequest.compile.rootResourcePath = 'foo/../../bar.tex' this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('relative path in root resource') .should.equal(true) }) }) - describe('with a root resource path that has unescaped + relative path', function() { - beforeEach(function() { + describe('with a root resource path that has unescaped + relative path', function () { + beforeEach(function () { this.validRequest.compile.rootResourcePath = 'foo/#../bar.tex' this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('relative path in root resource') .should.equal(true) }) }) - return describe('with an unknown syncType', function() { - beforeEach(function() { + return describe('with an unknown syncType', function () { + beforeEach(function () { this.validRequest.compile.options.syncType = 'unexpected' this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) }) - return it('should return an error', function() { + return it('should return an error', function () { return this.callback .calledWith('syncType attribute should be one of: full, incremental') .should.equal(true) diff --git a/test/unit/js/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js index efc4065b..ca2b625f 100644 --- a/test/unit/js/ResourceStateManagerTests.js +++ b/test/unit/js/ResourceStateManagerTests.js @@ -20,8 +20,8 @@ const modulePath = require('path').join( const Path = require('path') const Errors = require('../../../app/js/Errors') -describe('ResourceStateManager', function() { - beforeEach(function() { +describe('ResourceStateManager', function () { + beforeEach(function () { this.ResourceStateManager = SandboxedModule.require(modulePath, { singleOnly: true, requires: { @@ -42,13 +42,13 @@ describe('ResourceStateManager', function() { return (this.callback = sinon.stub()) }) - describe('saveProjectState', function() { - beforeEach(function() { + describe('saveProjectState', function () { + beforeEach(function () { return (this.fs.writeFile = sinon.stub().callsArg(2)) }) - describe('when the state is specified', function() { - beforeEach(function() { + describe('when the state is specified', function () { + beforeEach(function () { return this.ResourceStateManager.saveProjectState( this.state, this.resources, @@ -57,19 +57,19 @@ describe('ResourceStateManager', function() { ) }) - it('should write the resource list to disk', function() { + it('should write the resource list to disk', function () { return this.fs.writeFile .calledWith(this.resourceFileName, this.resourceFileContents) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - return describe('when the state is undefined', function() { - beforeEach(function() { + return describe('when the state is undefined', function () { + beforeEach(function () { this.state = undefined this.fs.unlink = sinon.stub().callsArg(1) return this.ResourceStateManager.saveProjectState( @@ -80,25 +80,25 @@ describe('ResourceStateManager', function() { ) }) - it('should unlink the resource file', function() { + it('should unlink the resource file', function () { return this.fs.unlink .calledWith(this.resourceFileName) .should.equal(true) }) - it('should not write the resource list to disk', function() { + it('should not write the resource list to disk', function () { return this.fs.writeFile.called.should.equal(false) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) }) - describe('checkProjectStateMatches', function() { - describe('when the state matches', function() { - beforeEach(function() { + describe('checkProjectStateMatches', function () { + describe('when the state matches', function () { + beforeEach(function () { this.SafeReader.readFile = sinon .stub() .callsArgWith(3, null, this.resourceFileContents) @@ -109,21 +109,21 @@ describe('ResourceStateManager', function() { ) }) - it('should read the resource file', function() { + it('should read the resource file', function () { return this.SafeReader.readFile .calledWith(this.resourceFileName) .should.equal(true) }) - return it('should call the callback with the results', function() { + return it('should call the callback with the results', function () { return this.callback .calledWithMatch(null, this.resources) .should.equal(true) }) }) - return describe('when the state does not match', function() { - beforeEach(function() { + return describe('when the state does not match', function () { + beforeEach(function () { this.SafeReader.readFile = sinon .stub() .callsArgWith(3, null, this.resourceFileContents) @@ -134,7 +134,7 @@ describe('ResourceStateManager', function() { ) }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { this.callback .calledWith(sinon.match(Errors.FilesOutOfSyncError)) .should.equal(true) @@ -145,9 +145,9 @@ describe('ResourceStateManager', function() { }) }) - return describe('checkResourceFiles', function() { - describe('when all the files are present', function() { - beforeEach(function() { + return describe('checkResourceFiles', function () { + describe('when all the files are present', function () { + beforeEach(function () { this.allFiles = [ this.resources[0].path, this.resources[1].path, @@ -161,13 +161,13 @@ describe('ResourceStateManager', function() { ) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.calledWithExactly().should.equal(true) }) }) - describe('when there is a missing file', function() { - beforeEach(function() { + describe('when there is a missing file', function () { + beforeEach(function () { this.allFiles = [this.resources[0].path, this.resources[1].path] this.fs.stat = sinon.stub().callsArgWith(1, new Error()) return this.ResourceStateManager.checkResourceFiles( @@ -178,7 +178,7 @@ describe('ResourceStateManager', function() { ) }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { this.callback .calledWith(sinon.match(Errors.FilesOutOfSyncError)) .should.equal(true) @@ -190,8 +190,8 @@ describe('ResourceStateManager', function() { }) }) - return describe('when a resource contains a relative path', function() { - beforeEach(function() { + return describe('when a resource contains a relative path', function () { + beforeEach(function () { this.resources[0].path = '../foo/bar.tex' this.allFiles = [ this.resources[0].path, @@ -206,7 +206,7 @@ describe('ResourceStateManager', function() { ) }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) const message = this.callback.args[0][0].message diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 36a95136..030fe70e 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -20,8 +20,8 @@ const modulePath = require('path').join( ) const path = require('path') -describe('ResourceWriter', function() { - beforeEach(function() { +describe('ResourceWriter', function () { + beforeEach(function () { let Timer this.ResourceWriter = SandboxedModule.require(modulePath, { singleOnly: true, @@ -37,7 +37,7 @@ describe('ResourceWriter', function() { 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, './Metrics': (this.Metrics = { inc: sinon.stub(), - Timer: (Timer = (function() { + Timer: (Timer = (function () { Timer = class Timer { static initClass() { this.prototype.done = sinon.stub() @@ -54,8 +54,8 @@ describe('ResourceWriter', function() { return (this.callback = sinon.stub()) }) - describe('syncResourcesToDisk on a full request', function() { - beforeEach(function() { + describe('syncResourcesToDisk on a full request', function () { + beforeEach(function () { this.resources = ['resource-1-mock', 'resource-2-mock', 'resource-3-mock'] this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) @@ -71,33 +71,33 @@ describe('ResourceWriter', function() { ) }) - it('should remove old files', function() { + it('should remove old files', function () { return this.ResourceWriter._removeExtraneousFiles .calledWith(this.resources, this.basePath) .should.equal(true) }) - it('should write each resource to disk', function() { - return Array.from(this.resources).map(resource => + it('should write each resource to disk', function () { + return Array.from(this.resources).map((resource) => this.ResourceWriter._writeResourceToDisk .calledWith(this.project_id, resource, this.basePath) .should.equal(true) ) }) - it('should store the sync state and resource list', function() { + it('should store the sync state and resource list', function () { return this.ResourceStateManager.saveProjectState .calledWith(this.syncState, this.resources, this.basePath) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - describe('syncResourcesToDisk on an incremental update', function() { - beforeEach(function() { + describe('syncResourcesToDisk on an incremental update', function () { + beforeEach(function () { this.resources = ['resource-1-mock'] this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) this.ResourceWriter._removeExtraneousFiles = sinon @@ -120,39 +120,39 @@ describe('ResourceWriter', function() { ) }) - it('should check the sync state matches', function() { + it('should check the sync state matches', function () { return this.ResourceStateManager.checkProjectStateMatches .calledWith(this.syncState, this.basePath) .should.equal(true) }) - it('should remove old files', function() { + it('should remove old files', function () { return this.ResourceWriter._removeExtraneousFiles .calledWith(this.resources, this.basePath) .should.equal(true) }) - it('should check each resource exists', function() { + it('should check each resource exists', function () { return this.ResourceStateManager.checkResourceFiles .calledWith(this.resources, this.allFiles, this.basePath) .should.equal(true) }) - it('should write each resource to disk', function() { - return Array.from(this.resources).map(resource => + it('should write each resource to disk', function () { + return Array.from(this.resources).map((resource) => this.ResourceWriter._writeResourceToDisk .calledWith(this.project_id, resource, this.basePath) .should.equal(true) ) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - describe('syncResourcesToDisk on an incremental update when the state does not match', function() { - beforeEach(function() { + describe('syncResourcesToDisk on an incremental update when the state does not match', function () { + beforeEach(function () { this.resources = ['resource-1-mock'] this.ResourceStateManager.checkProjectStateMatches = sinon .stub() @@ -169,19 +169,19 @@ describe('ResourceWriter', function() { ) }) - it('should check whether the sync state matches', function() { + it('should check whether the sync state matches', function () { return this.ResourceStateManager.checkProjectStateMatches .calledWith(this.syncState, this.basePath) .should.equal(true) }) - return it('should call the callback with an error', function() { + return it('should call the callback with an error', function () { return this.callback.calledWith(this.error).should.equal(true) }) }) - describe('_removeExtraneousFiles', function() { - beforeEach(function() { + describe('_removeExtraneousFiles', function () { + beforeEach(function () { this.output_files = [ { path: 'output.pdf', @@ -250,49 +250,49 @@ describe('ResourceWriter', function() { ) }) - it('should find the existing output files', function() { + it('should find the existing output files', function () { return this.OutputFileFinder.findOutputFiles .calledWith(this.resources, this.basePath) .should.equal(true) }) - it('should delete the output files', function() { + it('should delete the output files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'output.pdf')) .should.equal(true) }) - it('should delete the stdout log file', function() { + it('should delete the stdout log file', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'output.stdout')) .should.equal(true) }) - it('should delete the stderr log file', function() { + it('should delete the stderr log file', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'output.stderr')) .should.equal(true) }) - it('should delete the extra files', function() { + it('should delete the extra files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'extra/file.tex')) .should.equal(true) }) - it('should not delete the extra aux files', function() { + it('should not delete the extra aux files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'extra.aux')) .should.equal(false) }) - it('should not delete the knitr cache file', function() { + it('should not delete the knitr cache file', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'cache/_chunk1')) .should.equal(false) }) - it('should not delete the epstopdf converted files', function() { + it('should not delete the epstopdf converted files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith( path.join(this.basePath, 'figures/image-eps-converted-to.pdf') @@ -300,25 +300,25 @@ describe('ResourceWriter', function() { .should.equal(false) }) - it('should not delete the tikz md5 files', function() { + it('should not delete the tikz md5 files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'foo/main-figure0.md5')) .should.equal(false) }) - it('should not delete the tikz dpth files', function() { + it('should not delete the tikz dpth files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'foo/main-figure0.dpth')) .should.equal(false) }) - it('should not delete the tikz pdf files', function() { + it('should not delete the tikz pdf files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, 'foo/main-figure0.pdf')) .should.equal(false) }) - it('should not delete the minted pygstyle files', function() { + it('should not delete the minted pygstyle files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith( path.join(this.basePath, '_minted-main/default-pyg-prefix.pygstyle') @@ -326,13 +326,13 @@ describe('ResourceWriter', function() { .should.equal(false) }) - it('should not delete the minted default pygstyle files', function() { + it('should not delete the minted default pygstyle files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith(path.join(this.basePath, '_minted-main/default.pygstyle')) .should.equal(false) }) - it('should not delete the minted default pygtex files', function() { + it('should not delete the minted default pygtex files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith( path.join( @@ -343,7 +343,7 @@ describe('ResourceWriter', function() { .should.equal(false) }) - it('should not delete the markdown md.tex files', function() { + it('should not delete the markdown md.tex files', function () { return this.ResourceWriter._deleteFileIfNotDirectory .calledWith( path.join( @@ -354,18 +354,18 @@ describe('ResourceWriter', function() { .should.equal(false) }) - it('should call the callback', function() { + it('should call the callback', function () { return this.callback.called.should.equal(true) }) - return it('should time the request', function() { + return it('should time the request', function () { return this.Metrics.Timer.prototype.done.called.should.equal(true) }) }) - describe('_writeResourceToDisk', function() { - describe('with a url based resource', function() { - beforeEach(function() { + describe('_writeResourceToDisk', function () { + describe('with a url based resource', function () { + beforeEach(function () { this.fs.mkdir = sinon.stub().callsArg(2) this.resource = { path: 'main.tex', @@ -383,7 +383,7 @@ describe('ResourceWriter', function() { ) }) - it('should ensure the directory exists', function() { + it('should ensure the directory exists', function () { this.fs.mkdir .calledWith( path.dirname(path.join(this.basePath, this.resource.path)) @@ -391,7 +391,7 @@ describe('ResourceWriter', function() { .should.equal(true) }) - it('should write the URL from the cache', function() { + it('should write the URL from the cache', function () { return this.UrlCache.downloadUrlToFile .calledWith( this.project_id, @@ -402,17 +402,17 @@ describe('ResourceWriter', function() { .should.equal(true) }) - it('should call the callback', function() { + it('should call the callback', function () { return this.callback.called.should.equal(true) }) - return it('should not return an error if the resource writer errored', function() { + return it('should not return an error if the resource writer errored', function () { return should.not.exist(this.callback.args[0][0]) }) }) - describe('with a content based resource', function() { - beforeEach(function() { + describe('with a content based resource', function () { + beforeEach(function () { this.resource = { path: 'main.tex', content: 'Hello world' @@ -427,7 +427,7 @@ describe('ResourceWriter', function() { ) }) - it('should ensure the directory exists', function() { + it('should ensure the directory exists', function () { return this.fs.mkdir .calledWith( path.dirname(path.join(this.basePath, this.resource.path)) @@ -435,7 +435,7 @@ describe('ResourceWriter', function() { .should.equal(true) }) - it('should write the contents to disk', function() { + it('should write the contents to disk', function () { return this.fs.writeFile .calledWith( path.join(this.basePath, this.resource.path), @@ -444,13 +444,13 @@ describe('ResourceWriter', function() { .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - return describe('with a file path that breaks out of the root folder', function() { - beforeEach(function() { + return describe('with a file path that breaks out of the root folder', function () { + beforeEach(function () { this.resource = { path: '../../main.tex', content: 'Hello world' @@ -464,11 +464,11 @@ describe('ResourceWriter', function() { ) }) - it('should not write to disk', function() { + it('should not write to disk', function () { return this.fs.writeFile.called.should.equal(false) }) - it('should return an error', function() { + it('should return an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) const message = this.callback.args[0][0].message @@ -477,23 +477,23 @@ describe('ResourceWriter', function() { }) }) - return describe('checkPath', function() { - describe('with a valid path', function() { - beforeEach(function() { + return describe('checkPath', function () { + describe('with a valid path', function () { + beforeEach(function () { return this.ResourceWriter.checkPath('foo', 'bar', this.callback) }) - return it('should return the joined path', function() { + return it('should return the joined path', function () { return this.callback.calledWith(null, 'foo/bar').should.equal(true) }) }) - describe('with an invalid path', function() { - beforeEach(function() { + describe('with an invalid path', function () { + beforeEach(function () { this.ResourceWriter.checkPath('foo', 'baz/../../bar', this.callback) }) - it('should return an error', function() { + it('should return an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) const message = this.callback.args[0][0].message @@ -501,8 +501,8 @@ describe('ResourceWriter', function() { }) }) - describe('with another invalid path matching on a prefix', function() { - beforeEach(function() { + describe('with another invalid path matching on a prefix', function () { + beforeEach(function () { return this.ResourceWriter.checkPath( 'foo', '../foobar/baz', @@ -510,7 +510,7 @@ describe('ResourceWriter', function() { ) }) - it('should return an error', function() { + it('should return an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) const message = this.callback.args[0][0].message diff --git a/test/unit/js/StaticServerForbidSymlinksTests.js b/test/unit/js/StaticServerForbidSymlinksTests.js index b9545a4c..9a72168f 100644 --- a/test/unit/js/StaticServerForbidSymlinksTests.js +++ b/test/unit/js/StaticServerForbidSymlinksTests.js @@ -20,8 +20,8 @@ const modulePath = path.join( ) const { expect } = require('chai') -describe('StaticServerForbidSymlinks', function() { - beforeEach(function() { +describe('StaticServerForbidSymlinks', function () { + beforeEach(function () { this.settings = { path: { compilesDir: '/compiles/here' @@ -60,8 +60,8 @@ describe('StaticServerForbidSymlinks', function() { return (this.req.url = '/12345/output.pdf') }) - describe('sending a normal file through', function() { - beforeEach(function() { + describe('sending a normal file through', function () { + beforeEach(function () { return (this.fs.realpath = sinon .stub() .callsArgWith( @@ -71,8 +71,8 @@ describe('StaticServerForbidSymlinks', function() { )) }) - return it('should call next', function(done) { - this.res.sendStatus = function(resCode) { + return it('should call next', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(200) return done() } @@ -80,8 +80,8 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a missing file', function() { - beforeEach(function() { + describe('with a missing file', function () { + beforeEach(function () { return (this.fs.realpath = sinon .stub() .callsArgWith( @@ -91,8 +91,8 @@ describe('StaticServerForbidSymlinks', function() { )) }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -100,15 +100,15 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a symlink file', function() { - beforeEach(function() { + describe('with a symlink file', function () { + beforeEach(function () { return (this.fs.realpath = sinon .stub() .callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`)) }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -116,13 +116,13 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a relative file', function() { - beforeEach(function() { + describe('with a relative file', function () { + beforeEach(function () { return (this.req.url = '/12345/../67890/output.pdf') }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -130,13 +130,13 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a unnormalized file containing .', function() { - beforeEach(function() { + describe('with a unnormalized file containing .', function () { + beforeEach(function () { return (this.req.url = '/12345/foo/./output.pdf') }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -144,13 +144,13 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a file containing an empty path', function() { - beforeEach(function() { + describe('with a file containing an empty path', function () { + beforeEach(function () { return (this.req.url = '/12345/foo//output.pdf') }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -158,13 +158,13 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a non-project file', function() { - beforeEach(function() { + describe('with a non-project file', function () { + beforeEach(function () { return (this.req.url = '/.foo/output.pdf') }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -172,13 +172,13 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a file outside the compiledir', function() { - beforeEach(function() { + describe('with a file outside the compiledir', function () { + beforeEach(function () { return (this.req.url = '/../bar/output.pdf') }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -186,13 +186,13 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a file with no leading /', function() { - beforeEach(function() { + describe('with a file with no leading /', function () { + beforeEach(function () { return (this.req.url = './../bar/output.pdf') }) - return it('should send a 404', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 404', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(404) return done() } @@ -200,8 +200,8 @@ describe('StaticServerForbidSymlinks', function() { }) }) - describe('with a github style path', function() { - beforeEach(function() { + describe('with a github style path', function () { + beforeEach(function () { this.req.url = '/henryoswald-latex_example/output/output.log' return (this.fs.realpath = sinon .stub() @@ -212,8 +212,8 @@ describe('StaticServerForbidSymlinks', function() { )) }) - return it('should call next', function(done) { - this.res.sendStatus = function(resCode) { + return it('should call next', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(200) return done() } @@ -221,13 +221,13 @@ describe('StaticServerForbidSymlinks', function() { }) }) - return describe('with an error from fs.realpath', function() { - beforeEach(function() { + return describe('with an error from fs.realpath', function () { + beforeEach(function () { return (this.fs.realpath = sinon.stub().callsArgWith(1, 'error')) }) - return it('should send a 500', function(done) { - this.res.sendStatus = function(resCode) { + return it('should send a 500', function (done) { + this.res.sendStatus = function (resCode) { resCode.should.equal(500) return done() } diff --git a/test/unit/js/TikzManager.js b/test/unit/js/TikzManager.js index 1a9874cb..4304edc0 100644 --- a/test/unit/js/TikzManager.js +++ b/test/unit/js/TikzManager.js @@ -16,8 +16,8 @@ const modulePath = require('path').join( '../../../app/js/TikzManager' ) -describe('TikzManager', function() { - beforeEach(function() { +describe('TikzManager', function () { + beforeEach(function () { return (this.TikzManager = SandboxedModule.require(modulePath, { requires: { './ResourceWriter': (this.ResourceWriter = {}), @@ -28,15 +28,15 @@ describe('TikzManager', function() { })) }) - describe('checkMainFile', function() { - beforeEach(function() { + describe('checkMainFile', function () { + beforeEach(function () { this.compileDir = 'compile-dir' this.mainFile = 'main.tex' return (this.callback = sinon.stub()) }) - describe('if there is already an output.tex file in the resources', function() { - beforeEach(function() { + describe('if there is already an output.tex file in the resources', function () { + beforeEach(function () { this.resources = [{ path: 'main.tex' }, { path: 'output.tex' }] return this.TikzManager.checkMainFile( this.compileDir, @@ -46,13 +46,13 @@ describe('TikzManager', function() { ) }) - return it('should call the callback with false ', function() { + return it('should call the callback with false ', function () { return this.callback.calledWithExactly(null, false).should.equal(true) }) }) - return describe('if there is no output.tex file in the resources', function() { - beforeEach(function() { + return describe('if there is no output.tex file in the resources', function () { + beforeEach(function () { this.resources = [{ path: 'main.tex' }] return (this.ResourceWriter.checkPath = sinon .stub() @@ -60,8 +60,8 @@ describe('TikzManager', function() { .callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`)) }) - describe('and the main file contains tikzexternalize', function() { - beforeEach(function() { + describe('and the main file contains tikzexternalize', function () { + beforeEach(function () { this.SafeReader.readFile = sinon .stub() .withArgs(`${this.compileDir}/${this.mainFile}`) @@ -74,19 +74,19 @@ describe('TikzManager', function() { ) }) - it('should look at the file on disk', function() { + it('should look at the file on disk', function () { return this.SafeReader.readFile .calledWith(`${this.compileDir}/${this.mainFile}`) .should.equal(true) }) - return it('should call the callback with true ', function() { + return it('should call the callback with true ', function () { return this.callback.calledWithExactly(null, true).should.equal(true) }) }) - describe('and the main file does not contain tikzexternalize', function() { - beforeEach(function() { + describe('and the main file does not contain tikzexternalize', function () { + beforeEach(function () { this.SafeReader.readFile = sinon .stub() .withArgs(`${this.compileDir}/${this.mainFile}`) @@ -99,19 +99,19 @@ describe('TikzManager', function() { ) }) - it('should look at the file on disk', function() { + it('should look at the file on disk', function () { return this.SafeReader.readFile .calledWith(`${this.compileDir}/${this.mainFile}`) .should.equal(true) }) - return it('should call the callback with false', function() { + return it('should call the callback with false', function () { return this.callback.calledWithExactly(null, false).should.equal(true) }) }) - return describe('and the main file contains \\usepackage{pstool}', function() { - beforeEach(function() { + return describe('and the main file contains \\usepackage{pstool}', function () { + beforeEach(function () { this.SafeReader.readFile = sinon .stub() .withArgs(`${this.compileDir}/${this.mainFile}`) @@ -124,21 +124,21 @@ describe('TikzManager', function() { ) }) - it('should look at the file on disk', function() { + it('should look at the file on disk', function () { return this.SafeReader.readFile .calledWith(`${this.compileDir}/${this.mainFile}`) .should.equal(true) }) - return it('should call the callback with true ', function() { + return it('should call the callback with true ', function () { return this.callback.calledWithExactly(null, true).should.equal(true) }) }) }) }) - return describe('injectOutputFile', function() { - beforeEach(function() { + return describe('injectOutputFile', function () { + beforeEach(function () { this.rootDir = '/mock' this.filename = 'filename.tex' this.callback = sinon.stub() @@ -162,25 +162,25 @@ Hello world ) }) - it('sould check the path', function() { + it('sould check the path', function () { return this.ResourceWriter.checkPath .calledWith(this.rootDir, this.filename) .should.equal(true) }) - it('should read the file', function() { + it('should read the file', function () { return this.fs.readFile .calledWith(`${this.rootDir}/${this.filename}`, 'utf8') .should.equal(true) }) - it('should write out the same file as output.tex', function() { + it('should write out the same file as output.tex', function () { return this.fs.writeFile .calledWith(`${this.rootDir}/output.tex`, this.content, { flag: 'wx' }) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index f5c0f3e2..023b92de 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -16,8 +16,8 @@ require('chai').should() const modulePath = require('path').join(__dirname, '../../../app/js/UrlCache') const { EventEmitter } = require('events') -describe('UrlCache', function() { - beforeEach(function() { +describe('UrlCache', function () { + beforeEach(function () { this.callback = sinon.stub() this.url = 'www.example.com/file' this.project_id = 'project-id-123' @@ -34,16 +34,16 @@ describe('UrlCache', function() { })) }) - describe('_doesUrlNeedDownloading', function() { - beforeEach(function() { + describe('_doesUrlNeedDownloading', function () { + beforeEach(function () { this.lastModified = new Date() return (this.lastModifiedRoundedToSeconds = new Date( Math.floor(this.lastModified.getTime() / 1000) * 1000 )) }) - describe('when URL does not exist in cache', function() { - beforeEach(function() { + describe('when URL does not exist in cache', function () { + beforeEach(function () { this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, null) return this.UrlCache._doesUrlNeedDownloading( this.project_id, @@ -53,21 +53,21 @@ describe('UrlCache', function() { ) }) - return it('should return the callback with true', function() { + return it('should return the callback with true', function () { return this.callback.calledWith(null, true).should.equal(true) }) }) - return describe('when URL does exist in cache', function() { - beforeEach(function() { + return describe('when URL does exist in cache', function () { + beforeEach(function () { this.urlDetails = {} return (this.UrlCache._findUrlDetails = sinon .stub() .callsArgWith(2, null, this.urlDetails)) }) - describe('when the modified date is more recent than the cached modified date', function() { - beforeEach(function() { + describe('when the modified date is more recent than the cached modified date', function () { + beforeEach(function () { this.urlDetails.lastModified = new Date( this.lastModified.getTime() - 1000 ) @@ -79,19 +79,19 @@ describe('UrlCache', function() { ) }) - it('should get the url details', function() { + it('should get the url details', function () { return this.UrlCache._findUrlDetails .calledWith(this.project_id, this.url) .should.equal(true) }) - return it('should return the callback with true', function() { + return it('should return the callback with true', function () { return this.callback.calledWith(null, true).should.equal(true) }) }) - describe('when the cached modified date is more recent than the modified date', function() { - beforeEach(function() { + describe('when the cached modified date is more recent than the modified date', function () { + beforeEach(function () { this.urlDetails.lastModified = new Date( this.lastModified.getTime() + 1000 ) @@ -103,13 +103,13 @@ describe('UrlCache', function() { ) }) - return it('should return the callback with false', function() { + return it('should return the callback with false', function () { return this.callback.calledWith(null, false).should.equal(true) }) }) - describe('when the cached modified date is equal to the modified date', function() { - beforeEach(function() { + describe('when the cached modified date is equal to the modified date', function () { + beforeEach(function () { this.urlDetails.lastModified = this.lastModified return this.UrlCache._doesUrlNeedDownloading( this.project_id, @@ -119,13 +119,13 @@ describe('UrlCache', function() { ) }) - return it('should return the callback with false', function() { + return it('should return the callback with false', function () { return this.callback.calledWith(null, false).should.equal(true) }) }) - describe('when the provided modified date does not exist', function() { - beforeEach(function() { + describe('when the provided modified date does not exist', function () { + beforeEach(function () { this.lastModified = null return this.UrlCache._doesUrlNeedDownloading( this.project_id, @@ -135,13 +135,13 @@ describe('UrlCache', function() { ) }) - return it('should return the callback with true', function() { + return it('should return the callback with true', function () { return this.callback.calledWith(null, true).should.equal(true) }) }) - return describe('when the URL does not have a modified date', function() { - beforeEach(function() { + return describe('when the URL does not have a modified date', function () { + beforeEach(function () { this.urlDetails.lastModified = null return this.UrlCache._doesUrlNeedDownloading( this.project_id, @@ -151,23 +151,23 @@ describe('UrlCache', function() { ) }) - return it('should return the callback with true', function() { + return it('should return the callback with true', function () { return this.callback.calledWith(null, true).should.equal(true) }) }) }) }) - describe('_ensureUrlIsInCache', function() { - beforeEach(function() { + describe('_ensureUrlIsInCache', function () { + beforeEach(function () { this.UrlFetcher.pipeUrlToFileWithRetry = sinon.stub().callsArg(2) return (this.UrlCache._updateOrCreateUrlDetails = sinon .stub() .callsArg(3)) }) - describe('when the URL needs updating', function() { - beforeEach(function() { + describe('when the URL needs updating', function () { + beforeEach(function () { this.UrlCache._doesUrlNeedDownloading = sinon .stub() .callsArgWith(3, null, true) @@ -179,7 +179,7 @@ describe('UrlCache', function() { ) }) - it('should check that the url needs downloading', function() { + it('should check that the url needs downloading', function () { return this.UrlCache._doesUrlNeedDownloading .calledWith( this.project_id, @@ -189,7 +189,7 @@ describe('UrlCache', function() { .should.equal(true) }) - it('should download the URL to the cache file', function() { + it('should download the URL to the cache file', function () { return this.UrlFetcher.pipeUrlToFileWithRetry .calledWith( this.url, @@ -198,7 +198,7 @@ describe('UrlCache', function() { .should.equal(true) }) - it('should update the database entry', function() { + it('should update the database entry', function () { return this.UrlCache._updateOrCreateUrlDetails .calledWith( this.project_id, @@ -208,7 +208,7 @@ describe('UrlCache', function() { .should.equal(true) }) - return it('should return the callback with the cache file path', function() { + return it('should return the callback with the cache file path', function () { return this.callback .calledWith( null, @@ -218,8 +218,8 @@ describe('UrlCache', function() { }) }) - return describe('when the URL does not need updating', function() { - beforeEach(function() { + return describe('when the URL does not need updating', function () { + beforeEach(function () { this.UrlCache._doesUrlNeedDownloading = sinon .stub() .callsArgWith(3, null, false) @@ -231,11 +231,11 @@ describe('UrlCache', function() { ) }) - it('should not download the URL to the cache file', function() { + it('should not download the URL to the cache file', function () { return this.UrlFetcher.pipeUrlToFileWithRetry.called.should.equal(false) }) - return it('should return the callback with the cache file path', function() { + return it('should return the callback with the cache file path', function () { return this.callback .calledWith( null, @@ -246,8 +246,8 @@ describe('UrlCache', function() { }) }) - describe('downloadUrlToFile', function() { - beforeEach(function() { + describe('downloadUrlToFile', function () { + beforeEach(function () { this.cachePath = 'path/to/cached/url' this.destPath = 'path/to/destination' this.UrlCache._copyFile = sinon.stub().callsArg(2) @@ -263,25 +263,25 @@ describe('UrlCache', function() { ) }) - it('should ensure the URL is downloaded and updated in the cache', function() { + it('should ensure the URL is downloaded and updated in the cache', function () { return this.UrlCache._ensureUrlIsInCache .calledWith(this.project_id, this.url, this.lastModified) .should.equal(true) }) - it('should copy the file to the new location', function() { + it('should copy the file to the new location', function () { return this.UrlCache._copyFile .calledWith(this.cachePath, this.destPath) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - describe('_deleteUrlCacheFromDisk', function() { - beforeEach(function() { + describe('_deleteUrlCacheFromDisk', function () { + beforeEach(function () { this.fs.unlink = sinon.stub().callsArg(1) return this.UrlCache._deleteUrlCacheFromDisk( this.project_id, @@ -290,7 +290,7 @@ describe('UrlCache', function() { ) }) - it('should delete the cache file', function() { + it('should delete the cache file', function () { return this.fs.unlink .calledWith( this.UrlCache._cacheFilePathForUrl(this.project_id, this.url) @@ -298,13 +298,13 @@ describe('UrlCache', function() { .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - describe('_clearUrlFromCache', function() { - beforeEach(function() { + describe('_clearUrlFromCache', function () { + beforeEach(function () { this.UrlCache._deleteUrlCacheFromDisk = sinon.stub().callsArg(2) this.UrlCache._clearUrlDetails = sinon.stub().callsArg(2) return this.UrlCache._clearUrlFromCache( @@ -314,25 +314,25 @@ describe('UrlCache', function() { ) }) - it('should delete the file on disk', function() { + it('should delete the file on disk', function () { return this.UrlCache._deleteUrlCacheFromDisk .calledWith(this.project_id, this.url) .should.equal(true) }) - it('should clear the entry in the database', function() { + it('should clear the entry in the database', function () { return this.UrlCache._clearUrlDetails .calledWith(this.project_id, this.url) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - return describe('clearProject', function() { - beforeEach(function() { + return describe('clearProject', function () { + beforeEach(function () { this.urls = ['www.example.com/file1', 'www.example.com/file2'] this.UrlCache._findAllUrlsInProject = sinon .stub() @@ -341,15 +341,15 @@ describe('UrlCache', function() { return this.UrlCache.clearProject(this.project_id, this.callback) }) - it('should clear the cache for each url in the project', function() { - return Array.from(this.urls).map(url => + it('should clear the cache for each url in the project', function () { + return Array.from(this.urls).map((url) => this.UrlCache._clearUrlFromCache .calledWith(this.project_id, url) .should.equal(true) ) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index 57bee3a7..96ef4574 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -15,8 +15,8 @@ require('chai').should() const modulePath = require('path').join(__dirname, '../../../app/js/UrlFetcher') const { EventEmitter } = require('events') -describe('UrlFetcher', function() { - beforeEach(function() { +describe('UrlFetcher', function () { + beforeEach(function () { this.callback = sinon.stub() this.url = 'https://www.example.com/file/here?query=string' return (this.UrlFetcher = SandboxedModule.require(modulePath, { @@ -33,34 +33,34 @@ describe('UrlFetcher', function() { } })) }) - describe('pipeUrlToFileWithRetry', function() { - this.beforeEach(function() { + describe('pipeUrlToFileWithRetry', function () { + this.beforeEach(function () { this.UrlFetcher.pipeUrlToFile = sinon.stub() }) - it('should call pipeUrlToFile', function(done) { + it('should call pipeUrlToFile', function (done) { this.UrlFetcher.pipeUrlToFile.callsArgWith(2) - this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, (err) => { expect(err).to.equal(undefined) this.UrlFetcher.pipeUrlToFile.called.should.equal(true) done() }) }) - it('should call pipeUrlToFile multiple times on error', function(done) { + it('should call pipeUrlToFile multiple times on error', function (done) { const error = new Error("couldn't download file") this.UrlFetcher.pipeUrlToFile.callsArgWith(2, error) - this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, (err) => { expect(err).to.equal(error) this.UrlFetcher.pipeUrlToFile.callCount.should.equal(3) done() }) }) - it('should call pipeUrlToFile twice if only 1 error', function(done) { + it('should call pipeUrlToFile twice if only 1 error', function (done) { this.UrlFetcher.pipeUrlToFile.onCall(0).callsArgWith(2, 'error') this.UrlFetcher.pipeUrlToFile.onCall(1).callsArgWith(2) - this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, (err) => { expect(err).to.equal(undefined) this.UrlFetcher.pipeUrlToFile.callCount.should.equal(2) done() @@ -68,13 +68,13 @@ describe('UrlFetcher', function() { }) }) - describe('pipeUrlToFile', function() { - it('should turn off the cookie jar in request', function() { + describe('pipeUrlToFile', function () { + it('should turn off the cookie jar in request', function () { return this.defaults.calledWith({ jar: false }).should.equal(true) }) - describe('rewrite url domain if filestoreDomainOveride is set', function() { - beforeEach(function() { + describe('rewrite url domain if filestoreDomainOveride is set', function () { + beforeEach(function () { this.path = '/path/to/file/on/disk' this.request.get = sinon .stub() @@ -88,7 +88,7 @@ describe('UrlFetcher', function() { return (this.fs.unlink = (file, callback) => callback()) }) - it('should use the normal domain when override not set', function(done) { + it('should use the normal domain when override not set', function (done) { this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { this.request.get.args[0][0].url.should.equal(this.url) return done() @@ -99,7 +99,7 @@ describe('UrlFetcher', function() { return this.fileStream.emit('finish') }) - return it('should use override domain when filestoreDomainOveride is set', function(done) { + return it('should use override domain when filestoreDomainOveride is set', function (done) { this.settings.filestoreDomainOveride = '192.11.11.11' this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { this.request.get.args[0][0].url.should.equal( @@ -114,8 +114,8 @@ describe('UrlFetcher', function() { }) }) - return describe('pipeUrlToFile', function() { - beforeEach(function(done) { + return describe('pipeUrlToFile', function () { + beforeEach(function (done) { this.path = '/path/to/file/on/disk' this.request.get = sinon .stub() @@ -130,8 +130,8 @@ describe('UrlFetcher', function() { return done() }) - describe('successfully', function() { - beforeEach(function(done) { + describe('successfully', function () { + beforeEach(function (done) { this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { this.callback() return done() @@ -142,32 +142,32 @@ describe('UrlFetcher', function() { return this.fileStream.emit('finish') }) - it('should request the URL', function() { + it('should request the URL', function () { return this.request.get .calledWith(sinon.match({ url: this.url })) .should.equal(true) }) - it('should open the file for writing', function() { + it('should open the file for writing', function () { return this.fs.createWriteStream .calledWith(this.path) .should.equal(true) }) - it('should pipe the URL to the file', function() { + it('should pipe the URL to the file', function () { return this.urlStream.pipe .calledWith(this.fileStream) .should.equal(true) }) - return it('should call the callback', function() { + return it('should call the callback', function () { return this.callback.called.should.equal(true) }) }) - describe('with non success status code', function() { - beforeEach(function(done) { - this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { + describe('with non success status code', function () { + beforeEach(function (done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, (err) => { this.callback(err) return done() }) @@ -176,7 +176,7 @@ describe('UrlFetcher', function() { return this.urlStream.emit('end') }) - it('should call the callback with an error', function() { + it('should call the callback with an error', function () { this.callback.calledWith(sinon.match(Error)).should.equal(true) const message = this.callback.args[0][0].message @@ -186,9 +186,9 @@ describe('UrlFetcher', function() { }) }) - return describe('with error', function() { - beforeEach(function(done) { - this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { + return describe('with error', function () { + beforeEach(function (done) { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, (err) => { this.callback(err) return done() }) @@ -198,11 +198,11 @@ describe('UrlFetcher', function() { ) }) - it('should call the callback with the error', function() { + it('should call the callback with the error', function () { return this.callback.calledWith(this.error).should.equal(true) }) - return it('should only call the callback once, even if end is called', function() { + return it('should only call the callback once, even if end is called', function () { this.urlStream.emit('end') return this.callback.calledOnce.should.equal(true) }) From 57dfd9c01bac5f973e414bc79a3a8a85e0d1bc6b Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@overleaf.com> Date: Wed, 12 Aug 2020 15:11:21 +0100 Subject: [PATCH 565/709] [misc] bump logger-sharelatex to version 2.2.0 --- package-lock.json | 693 ++++++++++++++++++++++++++++++++-------------- package.json | 2 +- 2 files changed, 481 insertions(+), 214 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc358212..e185ad12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -163,9 +163,9 @@ } }, "@google-cloud/common": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz", - "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz", + "integrity": "sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg==", "requires": { "@google-cloud/projectify": "^1.0.0", "@google-cloud/promisify": "^1.0.0", @@ -175,7 +175,44 @@ "extend": "^3.0.2", "google-auth-library": "^5.5.0", "retry-request": "^4.0.0", - "teeny-request": "^5.2.1" + "teeny-request": "^6.0.0" + }, + "dependencies": { + "google-auth-library": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", + "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^2.1.0", + "gcp-metadata": "^3.4.0", + "gtoken": "^4.1.0", + "jws": "^4.0.0", + "lru-cache": "^5.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } } }, "@google-cloud/debug-agent": { @@ -346,44 +383,86 @@ } }, "@google-cloud/logging": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-5.5.3.tgz", - "integrity": "sha512-TZ/DzHod4icaC7wEEBm0PHYfbhvg0CbCVzKLsdAwj11xSD/egGNOsG5optEQcbAQEPrO1B5xBXfsE0wIBBYjpQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-7.3.0.tgz", + "integrity": "sha512-xTW1V4MKpYC0mjSugyuiyUoZ9g6A42IhrrO3z7Tt3SmAb2IRj2Gf4RLoguKKncs340ooZFXrrVN/++t2Aj5zgg==", "requires": { "@google-cloud/common": "^2.2.2", "@google-cloud/paginator": "^2.0.0", "@google-cloud/projectify": "^1.0.0", "@google-cloud/promisify": "^1.0.0", - "@opencensus/propagation-stackdriver": "0.0.18", + "@opencensus/propagation-stackdriver": "0.0.20", "arrify": "^2.0.0", "dot-prop": "^5.1.0", - "eventid": "^0.1.2", + "eventid": "^1.0.0", "extend": "^3.0.2", "gcp-metadata": "^3.1.0", - "google-gax": "^1.7.5", + "google-auth-library": "^5.2.2", + "google-gax": "^1.11.0", "is": "^3.3.0", "on-finished": "^2.3.0", - "protobufjs": "^6.8.8", "pumpify": "^2.0.0", "snakecase-keys": "^3.0.0", "stream-events": "^1.0.4", "through2": "^3.0.0", - "type-fest": "^0.8.0" + "type-fest": "^0.12.0" + }, + "dependencies": { + "google-auth-library": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", + "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^2.1.0", + "gcp-metadata": "^3.4.0", + "gtoken": "^4.1.0", + "jws": "^4.0.0", + "lru-cache": "^5.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "type-fest": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", + "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==" + } } }, "@google-cloud/logging-bunyan": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-2.0.0.tgz", - "integrity": "sha512-9W9B8GQNMlBdQSV+c0492+sMMknn4/428EdSO1xv5Hn07P32N/e4T25y4Gnl9jlrItuZHIXRwYPVSHqUyGb1Zg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-3.0.0.tgz", + "integrity": "sha512-ZLVXEejNQ27ktGcA3S/sd7GPefp7kywbn+/KoBajdb1Syqcmtc98jhXpYQBXVtNP2065iyu77s4SBaiYFbTC5A==", "requires": { - "@google-cloud/logging": "^5.5.2", - "google-auth-library": "^5.0.0" + "@google-cloud/logging": "^7.0.0", + "google-auth-library": "^6.0.0" } }, "@google-cloud/paginator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.1.tgz", - "integrity": "sha512-HZ6UTGY/gHGNriD7OCikYWL/Eu0sTEur2qqse2w6OVsz+57se3nTkqH14JIPxtf0vlEJ8IJN5w3BdZ22pjCB8g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz", + "integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==", "requires": { "arrify": "^2.0.0", "extend": "^3.0.2" @@ -557,14 +636,14 @@ } }, "@google-cloud/projectify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.1.tgz", - "integrity": "sha512-xknDOmsMgOYHksKc1GPbwDLsdej8aRNIA17SlSZgQdyrcC0lx0OGo4VZgYfwoEU1YS8oUxF9Y+6EzDOb0eB7Xg==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", + "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==" }, "@google-cloud/promisify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz", - "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", + "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==" }, "@google-cloud/trace-agent": { "version": "3.6.1", @@ -728,9 +807,9 @@ } }, "@grpc/grpc-js": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.9.tgz", - "integrity": "sha512-r1nDOEEiYmAsVYBaS4DPPqdwPOXPw7YhVOnnpPdWhlNtKbYzPash6DqWTTza9gBiYMA5d2Wiq6HzrPqsRaP4yA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", + "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", "requires": { "semver": "^6.2.0" }, @@ -743,18 +822,18 @@ } }, "@grpc/proto-loader": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.2.tgz", - "integrity": "sha512-eBKD/FPxQoY1x6QONW2nBd54QUEyzcFP9FenujmoeDPy1rutVSHki1s/wR68F6O1QfCNDx+ayBH1O2CVNMzyyw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", + "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", "requires": { "lodash.camelcase": "^4.3.0", "protobufjs": "^6.8.6" } }, "@opencensus/core": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.18.tgz", - "integrity": "sha512-PgRQXLyb3bLi8Z6pQct9erYFRdnYAZNQXAEVPf6Xq6IMkZaH20wiOTNNPxEckjI31mq5utgstAbwOn4gJiPjBQ==", + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.20.tgz", + "integrity": "sha512-vqOuTd2yuMpKohp8TNNGUAPjWEGjlnGfB9Rh5e3DKqeyR94YgierNs4LbMqxKtsnwB8Dm2yoEtRuUgoe5vD9DA==", "requires": { "continuation-local-storage": "^3.2.1", "log-driver": "^1.2.7", @@ -771,19 +850,19 @@ } }, "@opencensus/propagation-stackdriver": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.18.tgz", - "integrity": "sha512-BLwfszIGAfqN2mqGf/atfEu84cWeoLM/YuXGfXDO1iDN2k5GXz4QFyhS8sz5l63HtsYuQqFuV+Ze7ZM0NvJp2A==", + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.20.tgz", + "integrity": "sha512-P8yuHSLtce+yb+2EZjtTVqG7DQ48laC+IuOWi3X9q78s1Gni5F9+hmbmyP6Nb61jb5BEvXQX1s2rtRI6bayUWA==", "requires": { - "@opencensus/core": "^0.0.18", + "@opencensus/core": "^0.0.20", "hex2dec": "^1.0.1", "uuid": "^3.2.1" } }, "@overleaf/o-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-2.1.0.tgz", - "integrity": "sha512-Zd9sks9LrLw8ErHt/cXeWIkyxWAqNAvNGn7wIjLQJH6TTEEW835PWOhpch+hQwwWsTxWIx/JDj+IpZ3ouw925g==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-3.0.0.tgz", + "integrity": "sha512-LsM2s6Iy9G97ktPo0ys4VxtI/m3ahc1ZHwjo5XnhXtjeIkkkVAehsrcRRoV/yWepPjymB0oZonhcfojpjYR/tg==" }, "@protobufjs/aspromise": { "version": "1.1.2", @@ -905,6 +984,11 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -935,6 +1019,14 @@ "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", "dev": true }, + "@types/fs-extra": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", + "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==", + "requires": { + "@types/node": "*" + } + }, "@types/json-schema": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", @@ -1534,6 +1626,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -1781,6 +1878,11 @@ "which": "^1.2.9" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "d64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz", @@ -1944,9 +2046,9 @@ } }, "dot-prop": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.1.1.tgz", - "integrity": "sha512-QCHI6Lkf+9fJMpwfAFsTvbiSh6ujoPmhCLiDvD/n4dGtLvHfhuBwPdN6z2x4YSOwwtTcLoO/LP70xELWGF/JVA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", "requires": { "is-obj": "^2.0.0" } @@ -1960,7 +2062,6 @@ "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", - "dev": true, "optional": true, "requires": { "nan": "^2.14.0" @@ -1970,7 +2071,6 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, "optional": true } } @@ -2613,9 +2713,9 @@ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, "eventid": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/eventid/-/eventid-0.1.2.tgz", - "integrity": "sha1-CyMtPiROpbHVKJhBQOpprH7IkhU=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventid/-/eventid-1.0.0.tgz", + "integrity": "sha512-4upSDsvpxhWPsmw4fsJCp0zj8S7I0qh1lCDTmZXP8V3TtryQKDI8CgQPN+e5JakbWwzaAX3lrdp2b3KSoMSUpw==", "requires": { "d64": "^1.0.0", "uuid": "^3.0.1" @@ -2927,23 +3027,23 @@ } }, "gaxios": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz", - "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", + "integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==", "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", - "https-proxy-agent": "^3.0.0", + "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", "node-fetch": "^2.3.0" } }, "gcp-metadata": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.0.tgz", - "integrity": "sha512-ympv+yQ6k5QuWCuwQqnGEvFGS7MBKdcQdj1i188v3bW9QLFIchTGaBCEZxSQapT0jffdn1vdt8oJhB5VBWQO1Q==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz", + "integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==", "requires": { - "gaxios": "^2.0.1", + "gaxios": "^2.1.0", "json-bigint": "^0.3.0" } }, @@ -3002,27 +3102,122 @@ "dev": true }, "google-auth-library": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", - "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.6.tgz", + "integrity": "sha512-fWYdRdg55HSJoRq9k568jJA1lrhg9i2xgfhVIMJbskUmbDpJGHsbv9l41DGhCDXM21F9Kn4kUwdysgxSYBYJUw==", "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.2.0", - "gtoken": "^4.1.0", - "jws": "^3.1.5", - "lru-cache": "^5.0.0" + "gaxios": "^3.0.0", + "gcp-metadata": "^4.1.0", + "gtoken": "^5.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "dependencies": { + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, + "gaxios": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.1.0.tgz", + "integrity": "sha512-DDTn3KXVJJigtz+g0J3vhcfbDbKtAroSTxauWsdnP57sM5KZ3d2c/3D9RKFJ86s43hfw6WULg6TXYw/AYiBlpA==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.4.tgz", + "integrity": "sha512-5J/GIH0yWt/56R3dNaNWPGQ/zXsZOddYECfJaqxFWgrZ9HC2Kvc5vl9upOgUUHKzURjAVf2N+f6tEJiojqXUuA==", + "requires": { + "gaxios": "^3.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-p12-pem": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.2.tgz", + "integrity": "sha512-tbjzndQvSIHGBLzHnhDs3cL4RBjLbLXc2pYvGH+imGVu5b4RMAttUTdnmW2UH0t11QeBTXZ7wlXPS7hrypO/tg==", + "requires": { + "node-forge": "^0.9.0" + } + }, + "gtoken": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.3.tgz", + "integrity": "sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg==", + "requires": { + "gaxios": "^3.0.0", + "google-p12-pem": "^3.0.0", + "jws": "^4.0.0", + "mime": "^2.2.0" + } + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } }, "google-gax": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.7.5.tgz", - "integrity": "sha512-Tz2DFs8umzDcCBTi2W1cY4vEgAKaYRj70g6Hh/MiiZaJizrly7PgyxsIYUGi7sOpEuAbARQymYKvy5mNi8hEbg==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.15.3.tgz", + "integrity": "sha512-3JKJCRumNm3x2EksUTw4P1Rad43FTpqrtW9jzpf3xSMYXx+ogaqTM1vGo7VixHB4xkAyATXVIa3OcNSh8H9zsQ==", "requires": { - "@grpc/grpc-js": "0.6.9", + "@grpc/grpc-js": "~1.0.3", "@grpc/proto-loader": "^0.5.1", + "@types/fs-extra": "^8.0.1", + "@types/long": "^4.0.0", "abort-controller": "^3.0.0", "duplexify": "^3.6.0", "google-auth-library": "^5.0.0", @@ -3030,12 +3225,79 @@ "lodash.at": "^4.6.0", "lodash.has": "^4.5.2", "node-fetch": "^2.6.0", - "protobufjs": "^6.8.8", + "protobufjs": "^6.8.9", "retry-request": "^4.0.0", "semver": "^6.0.0", "walkdir": "^0.4.0" }, "dependencies": { + "@types/node": { + "version": "13.13.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz", + "integrity": "sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw==" + }, + "google-auth-library": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", + "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^2.1.0", + "gcp-metadata": "^3.4.0", + "gtoken": "^4.1.0", + "jws": "^4.0.0", + "lru-cache": "^5.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "protobufjs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", + "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + } + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -3044,9 +3306,9 @@ } }, "google-p12-pem": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz", - "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", + "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", "requires": { "node-forge": "^0.9.0" } @@ -3057,20 +3319,39 @@ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "gtoken": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.0.tgz", - "integrity": "sha512-wqyn2gf5buzEZN4QNmmiiW2i2JkEdZnL7Z/9p44RtZqgt4077m4khRgAYNuu8cBwHWCc6MsP6eDUn/KkF6jFIw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", + "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", "requires": { - "gaxios": "^2.0.0", + "gaxios": "^2.1.0", "google-p12-pem": "^2.0.0", - "jws": "^3.1.5", + "jws": "^4.0.0", "mime": "^2.2.0" }, "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" } } }, @@ -3168,21 +3449,35 @@ } }, "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "requires": { - "agent-base": "4", - "debug": "3.1.0" + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" }, "dependencies": { + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + } + }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -3197,18 +3492,26 @@ } }, "https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "agent-base": "6", + "debug": "4" }, "dependencies": { + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + } + }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } @@ -3789,75 +4092,33 @@ } }, "logger-sharelatex": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.9.1.tgz", - "integrity": "sha512-9s6JQnH/PN+Js2CmI8+J3MQCTNlRzP2Dh4pcekXrV6Jm5J4HzyPi+6d3zfBskZ4NBmaUVw9hC4p5dmdaRmh4mQ==", - "requires": { - "@google-cloud/logging-bunyan": "^2.0.0", - "@overleaf/o-error": "^2.0.0", - "bunyan": "1.8.12", - "raven": "1.1.3", - "request": "2.88.0", - "yn": "^3.1.1" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-2.2.0.tgz", + "integrity": "sha512-ko+OmE25XHJJCiz1R9EgwlfM7J/5olpunUfR3WcfuqOQrcUqsdBrDA2sOytngT0ViwjCR0Fh4qZVPwEWfmrvwA==", + "requires": { + "@google-cloud/logging-bunyan": "^3.0.0", + "@overleaf/o-error": "^3.0.0", + "bunyan": "^1.8.14", + "node-fetch": "^2.6.0", + "raven": "^2.6.4", + "yn": "^4.0.0" }, "dependencies": { "bunyan": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", - "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz", + "integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==", "requires": { "dtrace-provider": "~0.8", - "moment": "^2.10.6", + "moment": "^2.19.3", "mv": "~2", "safe-json-stringify": "~1" } }, - "dtrace-provider": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", - "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "optional": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } + "yn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-4.0.0.tgz", + "integrity": "sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==" } } }, @@ -3926,11 +4187,6 @@ "yallist": "^3.0.2" } }, - "lsmod": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz", - "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=" - }, "lynx": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.2.0.tgz", @@ -3963,6 +4219,23 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==" }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + } + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5502,19 +5775,10 @@ "stream-shift": "^1.0.0" } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -5551,21 +5815,26 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raven": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/raven/-/raven-1.1.3.tgz", - "integrity": "sha1-QnPBrm005CMPUbLAEEGjK5Iygio=", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/raven/-/raven-2.6.4.tgz", + "integrity": "sha512-6PQdfC4+DQSFncowthLf+B6Hr0JpPsFBgTVYTAOq7tCmx/kR4SXbeawtPch20+3QfUcQDoJBLjWW1ybvZ4kXTw==", "requires": { "cookie": "0.3.1", - "json-stringify-safe": "5.0.1", - "lsmod": "1.0.0", - "stack-trace": "0.0.9", - "uuid": "3.0.0" + "md5": "^2.2.1", + "stack-trace": "0.0.10", + "timed-out": "4.0.1", + "uuid": "3.3.2" }, "dependencies": { + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, "uuid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", - "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -6095,9 +6364,9 @@ } }, "snakecase-keys": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.1.0.tgz", - "integrity": "sha512-QM038drLbhdOY5HcRQVjO1ZJ1WR7yV5D5TIBzcOB/g3f5HURHhfpYEnvOyzXet8K+MQsgeIUA7O7vn90nAX6EA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.2.0.tgz", + "integrity": "sha512-WTJ0NhCH/37J+PU3fuz0x5b6TvtWQChTcKPOndWoUy0pteKOe0hrHMzSRsJOWSIP48EQkzUEsgQPmrG3W8pFNQ==", "requires": { "map-obj": "^4.0.0", "to-snake-case": "^1.0.0" @@ -6211,7 +6480,8 @@ "stack-trace": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", + "dev": true }, "statsd-parser": { "version": "0.0.4", @@ -6480,15 +6750,22 @@ } }, "teeny-request": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.0.tgz", - "integrity": "sha512-sN9E3JvEBe2CFqB/jpJpw1erWD1C7MxyYCxogHFCQSyZfkHYcdf4wzVQSw7FZxbwcfnS+FP0W9BS0mp6SEOKjg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz", + "integrity": "sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw==", "requires": { - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", "node-fetch": "^2.2.0", "stream-events": "^1.0.5", - "uuid": "^3.3.2" + "uuid": "^7.0.0" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } } }, "text-table": { @@ -6510,6 +6787,11 @@ "readable-stream": "2 || 3" } }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + }, "timekeeper": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.2.0.tgz", @@ -6571,22 +6853,6 @@ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -6624,7 +6890,8 @@ "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true }, "type-is": { "version": "1.6.18", diff --git a/package.json b/package.json index a5260759..8b93ad75 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "heapdump": "^0.3.15", "lockfile": "^1.0.4", "lodash": "^4.17.15", - "logger-sharelatex": "^1.9.1", + "logger-sharelatex": "^2.2.0", "lynx": "0.2.0", "metrics-sharelatex": "^2.6.0", "mysql": "^2.18.1", From 004550f6e6acc2ce378b903fdd975d2169c69c68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2020 05:11:31 +0000 Subject: [PATCH 566/709] Bump lodash from 4.17.15 to 4.17.20 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.20. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.20) Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e185ad12..b4d1451d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4029,9 +4029,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.at": { "version": "4.6.0", diff --git a/package.json b/package.json index 8b93ad75..8c1e2f84 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "fs-extra": "^8.1.0", "heapdump": "^0.3.15", "lockfile": "^1.0.4", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "logger-sharelatex": "^2.2.0", "lynx": "0.2.0", "metrics-sharelatex": "^2.6.0", From db1dbdb42f43525526fd44c66af316988dc832e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2020 16:50:44 +0000 Subject: [PATCH 567/709] Bump bl from 4.0.1 to 4.0.3 Bumps [bl](https://github.com/rvagg/bl) from 4.0.1 to 4.0.3. - [Release notes](https://github.com/rvagg/bl/releases) - [Commits](https://github.com/rvagg/bl/compare/v4.0.1...v4.0.3) Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e185ad12..6501a57d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1437,13 +1437,20 @@ "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" }, "bl": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.1.tgz", - "integrity": "sha512-FL/TdvchukRCuWVxT0YMO/7+L5TNeNrVFvRU2IY63aUyv9mpt8splf2NEr6qXtPo5fya5a66YohQKvGNmLrWNA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", "readable-stream": "^3.4.0" }, "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -1518,6 +1525,15 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3531,6 +3547,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", From ca61354cfe789bda1ec7fa8550108e42904d2795 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Wed, 2 Sep 2020 16:45:49 -0400 Subject: [PATCH 568/709] Decaf cleanup: remove unnecessary Array.from() --- app/js/DockerRunner.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 72345392..f719ebd5 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -8,7 +8,6 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS205: Consider reworking code to avoid use of IIFEs @@ -77,7 +76,7 @@ module.exports = DockerRunner = { const volumes = {} volumes[directory] = '/compile' - command = Array.from(command).map((arg) => + command = command.map((arg) => __guardMethod__(arg.toString(), 'replace', (o) => o.replace('$COMPILE_DIR', '/compile') ) @@ -177,7 +176,7 @@ module.exports = DockerRunner = { _callback = function (error, output) {} } const callback = function (...args) { - _callback(...Array.from(args || [])) + _callback(...args) // Only call the callback once return (_callback = function () {}) } @@ -538,7 +537,7 @@ module.exports = DockerRunner = { _callback = function (error, exitCode) {} } const callback = function (...args) { - _callback(...Array.from(args || [])) + _callback(...args) // Only call the callback once return (_callback = function () {}) } @@ -668,7 +667,7 @@ module.exports = DockerRunner = { return callback(error) } const jobs = [] - for (const container of Array.from(containers || [])) { + for (const container of containers) { ;((container) => DockerRunner.examineOldContainer(container, function ( err, From 9ab5738ae3591f1d8dfcae651ea93eb9b9f76940 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Wed, 2 Sep 2020 16:58:41 -0400 Subject: [PATCH 569/709] Decaf cleanup: remove unnecessary returns --- app/js/DockerRunner.js | 89 ++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index f719ebd5..8ea3c62c 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -8,7 +8,6 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS205: Consider reworking code to avoid use of IIFEs * DS207: Consider shorter variations of null checks @@ -120,13 +119,11 @@ module.exports = DockerRunner = { { err: error, project_id }, 'error running container so destroying and retrying' ) - return DockerRunner.destroyContainer(name, null, true, function ( - error - ) { + DockerRunner.destroyContainer(name, null, true, function (error) { if (error != null) { return callback(error) } - return DockerRunner._runAndWaitForContainer( + DockerRunner._runAndWaitForContainer( options, volumes, timeout, @@ -134,12 +131,13 @@ module.exports = DockerRunner = { ) }) } else { - return callback(error, output) + callback(error, output) } }) + // pass back the container name to allow it to be killed return name - }, // pass back the container name to allow it to be killed + }, kill(container_id, callback) { if (callback == null) { @@ -147,7 +145,7 @@ module.exports = DockerRunner = { } logger.log({ container_id }, 'sending kill signal to container') const container = dockerode.getContainer(container_id) - return container.kill(function (error) { + container.kill(function (error) { if ( error != null && __guardMethod__( @@ -164,9 +162,9 @@ module.exports = DockerRunner = { } if (error != null) { logger.error({ err: error, container_id }, 'error killing container') - return callback(error) + callback(error) } else { - return callback() + callback() } }) }, @@ -178,7 +176,7 @@ module.exports = DockerRunner = { const callback = function (...args) { _callback(...args) // Only call the callback once - return (_callback = function () {}) + _callback = function () {} } const { name } = options @@ -189,7 +187,7 @@ module.exports = DockerRunner = { const callbackIfFinished = function () { if (streamEnded && containerReturned) { - return callback(null, output) + callback(null, output) } } @@ -199,10 +197,10 @@ module.exports = DockerRunner = { } output = _output streamEnded = true - return callbackIfFinished() + callbackIfFinished() } - return DockerRunner.startContainer( + DockerRunner.startContainer( options, volumes, attachStreamHandler, @@ -211,7 +209,7 @@ module.exports = DockerRunner = { return callback(error) } - return DockerRunner.waitForContainer(name, timeout, function ( + DockerRunner.waitForContainer(name, timeout, function ( error, exitCode ) { @@ -237,7 +235,7 @@ module.exports = DockerRunner = { (x) => (x.SecurityOpt = null) ) // small log line logger.log({ err, exitCode, options }, 'docker container has exited') - return callbackIfFinished() + callbackIfFinished() }) } ) @@ -364,7 +362,7 @@ module.exports = DockerRunner = { }, startContainer(options, volumes, attachStreamHandler, callback) { - return LockManager.runWithLock( + LockManager.runWithLock( options.name, (releaseLock) => // Check that volumes exist before starting the container. @@ -375,7 +373,7 @@ module.exports = DockerRunner = { if (err != null) { return releaseLock(err) } - return DockerRunner._startContainer( + DockerRunner._startContainer( options, volumes, attachStreamHandler, @@ -405,13 +403,13 @@ module.exports = DockerRunner = { if (!(stats != null ? stats.isDirectory() : undefined)) { return cb(DockerRunner.ERR_NOT_DIRECTORY) } - return cb() + cb() }) const jobs = [] for (const vol in volumes) { ;((vol) => jobs.push((cb) => checkVolume(vol, cb)))(vol) } - return async.series(jobs, callback) + async.series(jobs, callback) }, _startContainer(options, volumes, attachStreamHandler, callback) { @@ -429,7 +427,7 @@ module.exports = DockerRunner = { if (error != null) { return callback(error) } - return startExistingContainer() + startExistingContainer() }) var startExistingContainer = () => DockerRunner.attachToContainer( @@ -439,37 +437,37 @@ module.exports = DockerRunner = { if (error != null) { return callback(error) } - return container.start(function (error) { + container.start(function (error) { if ( error != null && (error != null ? error.statusCode : undefined) !== 304 ) { // already running - return callback(error) + callback(error) } else { - return callback() + callback() } }) } ) - return container.inspect(function (error, stats) { + container.inspect(function (error, stats) { if ((error != null ? error.statusCode : undefined) === 404) { - return createAndStartContainer() + createAndStartContainer() } else if (error != null) { logger.err( { container_name: name, error }, 'unable to inspect container to start' ) - return callback(error) + callback(error) } else { - return startExistingContainer() + startExistingContainer() } }) }, attachToContainer(containerId, attachStreamHandler, attachStartCallback) { const container = dockerode.getContainer(containerId) - return container.attach({ stdout: 1, stderr: 1, stream: 1 }, function ( + container.attach({ stdout: 1, stderr: 1, stream: 1 }, function ( error, stream ) { @@ -495,7 +493,7 @@ module.exports = DockerRunner = { return } if (this.data.length < MAX_OUTPUT) { - return (this.data += data) + this.data += data } else { logger.error( { @@ -506,7 +504,7 @@ module.exports = DockerRunner = { `${name} exceeds max size` ) this.data += `(...truncated at ${MAX_OUTPUT} chars...)` - return (this.overflowed = true) + this.overflowed = true } } // kill container if too much output @@ -526,7 +524,7 @@ module.exports = DockerRunner = { ) ) - return stream.on('end', () => + stream.on('end', () => attachStreamHandler(null, { stdout: stdout.data, stderr: stderr.data }) ) }) @@ -539,7 +537,7 @@ module.exports = DockerRunner = { const callback = function (...args) { _callback(...args) // Only call the callback once - return (_callback = function () {}) + _callback = function () {} } const container = dockerode.getContainer(containerId) @@ -551,11 +549,11 @@ module.exports = DockerRunner = { { container_id: containerId }, 'timeout reached, killing container' ) - return container.kill(function () {}) + container.kill(function () {}) }, timeout) logger.log({ container_id: containerId }, 'waiting for docker container') - return container.wait(function (error, res) { + container.wait(function (error, res) { if (error != null) { clearTimeout(timeoutId) logger.error( @@ -568,14 +566,14 @@ module.exports = DockerRunner = { logger.log({ containerId }, 'docker container timed out') error = DockerRunner.ERR_TIMED_OUT error.timedout = true - return callback(error) + callback(error) } else { clearTimeout(timeoutId) logger.log( { container_id: containerId, exitCode: res.StatusCode }, 'docker container returned' ) - return callback(null, res.StatusCode) + callback(null, res.StatusCode) } }) }, @@ -590,7 +588,7 @@ module.exports = DockerRunner = { if (callback == null) { callback = function (error) {} } - return LockManager.runWithLock( + LockManager.runWithLock( containerName, (releaseLock) => DockerRunner._destroyContainer( @@ -608,7 +606,7 @@ module.exports = DockerRunner = { } logger.log({ container_id: containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) - return container.remove({ force: shouldForce === true }, function (error) { + container.remove({ force: shouldForce === true }, function (error) { if ( error != null && (error != null ? error.statusCode : undefined) === 404 @@ -627,7 +625,7 @@ module.exports = DockerRunner = { } else { logger.log({ container_id: containerId }, 'destroyed container') } - return callback(error) + callback(error) }) }, @@ -652,17 +650,14 @@ module.exports = DockerRunner = { { containerName: name, created, now, age, maxAge, ttl }, 'checking whether to destroy container' ) - return callback(null, name, container.Id, ttl) + callback(null, name, container.Id, ttl) }, destroyOldContainers(callback) { if (callback == null) { callback = function (error) {} } - return dockerode.listContainers({ all: true }, function ( - error, - containers - ) { + dockerode.listContainers({ all: true }, function (error, containers) { if (error != null) { return callback(error) } @@ -679,7 +674,7 @@ module.exports = DockerRunner = { // strip the / prefix // the LockManager uses the plain container name name = name.slice(1) - return jobs.push((cb) => + jobs.push((cb) => DockerRunner.destroyContainer(name, id, false, () => cb()) ) } @@ -687,7 +682,7 @@ module.exports = DockerRunner = { } // Ignore errors because some containers get stuck but // will be destroyed next time - return async.series(jobs, callback) + async.series(jobs, callback) }) }, From 92fbb9581e628443febd024350abf4d16094f6ee Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Wed, 2 Sep 2020 17:06:35 -0400 Subject: [PATCH 570/709] Decaf cleanup: remove __guard__ --- app/js/DockerRunner.js | 43 +++++++++--------------------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 8ea3c62c..4c7ae2a5 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -8,7 +8,6 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS103: Rewrite code to no longer use __guard__ * DS205: Consider reworking code to avoid use of IIFEs * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md @@ -28,10 +27,9 @@ const _ = require('lodash') logger.info('using docker runner') const usingSiblingContainers = () => - __guard__( - Settings != null ? Settings.path : undefined, - (x) => x.sandboxedCompilesHostDir - ) != null + Settings != null && + Settings.path != null && + Settings.path.sandboxedCompilesHostDir != null let containerMonitorTimeout let containerMonitorInterval @@ -76,9 +74,7 @@ module.exports = DockerRunner = { volumes[directory] = '/compile' command = command.map((arg) => - __guardMethod__(arg.toString(), 'replace', (o) => - o.replace('$COMPILE_DIR', '/compile') - ) + arg.toString().replace('$COMPILE_DIR', '/compile') ) if (image == null) { ;({ image } = Settings.clsi.docker) @@ -148,11 +144,8 @@ module.exports = DockerRunner = { container.kill(function (error) { if ( error != null && - __guardMethod__( - error != null ? error.message : undefined, - 'match', - (o) => o.match(/Cannot kill container .* is not running/) - ) + error.message != null && + error.message.match(/Cannot kill container .* is not running/) ) { logger.warn( { err: error, container_id }, @@ -230,10 +223,9 @@ module.exports = DockerRunner = { return callback(err) } containerReturned = true - __guard__( - options != null ? options.HostConfig : undefined, - (x) => (x.SecurityOpt = null) - ) // small log line + if (options != null && options.HostConfig != null) { + options.HostConfig.SecurityOpt = null + } logger.log({ err, exitCode, options }, 'docker container has exited') callbackIfFinished() }) @@ -718,20 +710,3 @@ module.exports = DockerRunner = { } DockerRunner.startContainerMonitor() - -function __guard__(value, transform) { - return typeof value !== 'undefined' && value !== null - ? transform(value) - : undefined -} -function __guardMethod__(obj, methodName, transform) { - if ( - typeof obj !== 'undefined' && - obj !== null && - typeof obj[methodName] === 'function' - ) { - return transform(obj, methodName) - } else { - return undefined - } -} From 73cf107029f7f7800e78b4321296629dd0abb522 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Wed, 2 Sep 2020 17:16:24 -0400 Subject: [PATCH 571/709] Decaf cleanup: remove IIFEs --- app/js/DockerRunner.js | 54 ++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 4c7ae2a5..f2ffc58a 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -8,7 +8,6 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS205: Consider reworking code to avoid use of IIFEs * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ @@ -278,23 +277,11 @@ module.exports = DockerRunner = { NetworkDisabled: true, Memory: 1024 * 1024 * 1024 * 1024, // 1 Gb User: Settings.clsi.docker.user, - Env: (() => { - const result = [] - for (key in env) { - value = env[key] - result.push(`${key}=${value}`) - } - return result - })(), // convert the environment hash to an array + Env: Object.entries(env).map(([key, value]) => `${key}=${value}`), HostConfig: { - Binds: (() => { - const result1 = [] - for (hostVol in volumes) { - dockerVol = volumes[hostVol] - result1.push(`${hostVol}:${dockerVol}`) - } - return result1 - })(), + Binds: Object.entries(volumes).map( + ([hostVol, dockerVol]) => `${hostVol}:${dockerVol}` + ), LogConfig: { Type: 'none', Config: {} }, Ulimits: [ { @@ -399,7 +386,7 @@ module.exports = DockerRunner = { }) const jobs = [] for (const vol in volumes) { - ;((vol) => jobs.push((cb) => checkVolume(vol, cb)))(vol) + jobs.push((cb) => checkVolume(vol, cb)) } async.series(jobs, callback) }, @@ -655,22 +642,21 @@ module.exports = DockerRunner = { } const jobs = [] for (const container of containers) { - ;((container) => - DockerRunner.examineOldContainer(container, function ( - err, - name, - id, - ttl - ) { - if (name.slice(0, 9) === '/project-' && ttl <= 0) { - // strip the / prefix - // the LockManager uses the plain container name - name = name.slice(1) - jobs.push((cb) => - DockerRunner.destroyContainer(name, id, false, () => cb()) - ) - } - }))(container) + DockerRunner.examineOldContainer(container, function ( + err, + name, + id, + ttl + ) { + if (name.slice(0, 9) === '/project-' && ttl <= 0) { + // strip the / prefix + // the LockManager uses the plain container name + name = name.slice(1) + jobs.push((cb) => + DockerRunner.destroyContainer(name, id, false, () => cb()) + ) + } + }) } // Ignore errors because some containers get stuck but // will be destroyed next time From f2734c86caf0f73fdcec72a92acaada567d98346 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 14:43:04 -0400 Subject: [PATCH 572/709] Decaf cleanup: remove default callbacks --- app/js/DockerRunner.js | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index f2ffc58a..fbb851f8 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -50,9 +50,6 @@ module.exports = DockerRunner = { callback ) { let name - if (callback == null) { - callback = function (error, output) {} - } if (usingSiblingContainers()) { const _newPath = Settings.path.sandboxedCompilesHostDir logger.log( @@ -135,9 +132,6 @@ module.exports = DockerRunner = { }, kill(container_id, callback) { - if (callback == null) { - callback = function (error) {} - } logger.log({ container_id }, 'sending kill signal to container') const container = dockerode.getContainer(container_id) container.kill(function (error) { @@ -162,9 +156,6 @@ module.exports = DockerRunner = { }, _runAndWaitForContainer(options, volumes, timeout, _callback) { - if (_callback == null) { - _callback = function (error, output) {} - } const callback = function (...args) { _callback(...args) // Only call the callback once @@ -366,9 +357,6 @@ module.exports = DockerRunner = { // Check that volumes exist and are directories _checkVolumes(options, volumes, callback) { - if (callback == null) { - callback = function (error, containerName) {} - } if (usingSiblingContainers()) { // Server Pro, with sibling-containers active, skip checks return callback(null) @@ -392,9 +380,6 @@ module.exports = DockerRunner = { }, _startContainer(options, volumes, attachStreamHandler, callback) { - if (callback == null) { - callback = function (error, output) {} - } callback = _.once(callback) const { name } = options @@ -510,9 +495,6 @@ module.exports = DockerRunner = { }, waitForContainer(containerId, timeout, _callback) { - if (_callback == null) { - _callback = function (error, exitCode) {} - } const callback = function (...args) { _callback(...args) // Only call the callback once @@ -564,9 +546,6 @@ module.exports = DockerRunner = { // async exception, but if you delete by id it just does a normal // error callback. We fall back to deleting by name if no id is // supplied. - if (callback == null) { - callback = function (error) {} - } LockManager.runWithLock( containerName, (releaseLock) => @@ -580,9 +559,6 @@ module.exports = DockerRunner = { }, _destroyContainer(containerId, shouldForce, callback) { - if (callback == null) { - callback = function (error) {} - } logger.log({ container_id: containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) container.remove({ force: shouldForce === true }, function (error) { @@ -614,9 +590,6 @@ module.exports = DockerRunner = { Settings.clsi.docker.maxContainerAge || (oneHour = 60 * 60 * 1000), examineOldContainer(container, callback) { - if (callback == null) { - callback = function (error, name, id, ttl) {} - } const name = container.Name || (container.Names != null ? container.Names[0] : undefined) @@ -633,9 +606,6 @@ module.exports = DockerRunner = { }, destroyOldContainers(callback) { - if (callback == null) { - callback = function (error) {} - } dockerode.listContainers({ all: true }, function (error, containers) { if (error != null) { return callback(error) @@ -677,7 +647,12 @@ module.exports = DockerRunner = { const randomDelay = Math.floor(Math.random() * 5 * 60 * 1000) containerMonitorTimeout = setTimeout(() => { containerMonitorInterval = setInterval( - () => DockerRunner.destroyOldContainers(), + () => + DockerRunner.destroyOldContainers((err) => { + if (err) { + logger.error({ err }, 'failed to destroy old containers') + } + }), (oneHour = 60 * 60 * 1000) ) }, randomDelay) From 2a31f8c8d757d954dfe06f5c7f5b7c585de369b1 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 14:56:27 -0400 Subject: [PATCH 573/709] Decaf cleanup: simplify null checks --- app/js/DockerRunner.js | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index fbb851f8..9725fe93 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -4,13 +4,6 @@ no-return-assign, no-unused-vars, */ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ let DockerRunner, oneHour const Settings = require('settings-sharelatex') const logger = require('logger-sharelatex') @@ -286,10 +279,7 @@ module.exports = DockerRunner = { } } - if ( - (Settings.path != null ? Settings.path.synctexBinHostPath : undefined) != - null - ) { + if (Settings.path != null && Settings.path.synctexBinHostPath != null) { options.HostConfig.Binds.push( `${Settings.path.synctexBinHostPath}:/opt/synctex:ro` ) @@ -367,7 +357,7 @@ module.exports = DockerRunner = { if (err != null) { return cb(err) } - if (!(stats != null ? stats.isDirectory() : undefined)) { + if (!stats.isDirectory()) { return cb(DockerRunner.ERR_NOT_DIRECTORY) } cb() @@ -402,20 +392,17 @@ module.exports = DockerRunner = { return callback(error) } container.start(function (error) { - if ( - error != null && - (error != null ? error.statusCode : undefined) !== 304 - ) { - // already running + if (error != null && error.statusCode !== 304) { callback(error) } else { + // already running callback() } }) } ) container.inspect(function (error, stats) { - if ((error != null ? error.statusCode : undefined) === 404) { + if (error != null && error.statusCode === 404) { createAndStartContainer() } else if (error != null) { logger.err( @@ -562,10 +549,7 @@ module.exports = DockerRunner = { logger.log({ container_id: containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) container.remove({ force: shouldForce === true }, function (error) { - if ( - error != null && - (error != null ? error.statusCode : undefined) === 404 - ) { + if (error != null && error.statusCode === 404) { logger.warn( { err: error, container_id: containerId }, 'container not found, continuing' @@ -590,9 +574,7 @@ module.exports = DockerRunner = { Settings.clsi.docker.maxContainerAge || (oneHour = 60 * 60 * 1000), examineOldContainer(container, callback) { - const name = - container.Name || - (container.Names != null ? container.Names[0] : undefined) + const name = container.Name || (container.Names && container.Names[0]) const created = container.Created * 1000 // creation time is returned in seconds const now = Date.now() const age = now - created From d86a856997bc39042cbe16492e1d4bb44e1f795c Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 14:58:37 -0400 Subject: [PATCH 574/709] Decaf cleanup: camel-case variables --- app/js/DockerRunner.js | 52 +++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 9725fe93..f9977d80 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -1,5 +1,4 @@ /* eslint-disable - camelcase, handle-callback-err, no-return-assign, no-unused-vars, @@ -33,7 +32,7 @@ module.exports = DockerRunner = { ERR_TIMED_OUT: new Error('container timed out'), run( - project_id, + projectId, command, directory, image, @@ -90,18 +89,18 @@ module.exports = DockerRunner = { compileGroup ) const fingerprint = DockerRunner._fingerprintContainer(options) - options.name = name = `project-${project_id}-${fingerprint}` + options.name = name = `project-${projectId}-${fingerprint}` // logOptions = _.clone(options) // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" - logger.log({ project_id }, 'running docker container') + logger.log({ projectId }, 'running docker container') DockerRunner._runAndWaitForContainer(options, volumes, timeout, function ( error, output ) { if (error && error.statusCode === 500) { logger.log( - { err: error, project_id }, + { err: error, projectId }, 'error running container so destroying and retrying' ) DockerRunner.destroyContainer(name, null, true, function (error) { @@ -124,9 +123,9 @@ module.exports = DockerRunner = { return name }, - kill(container_id, callback) { - logger.log({ container_id }, 'sending kill signal to container') - const container = dockerode.getContainer(container_id) + kill(containerId, callback) { + logger.log({ containerId }, 'sending kill signal to container') + const container = dockerode.getContainer(containerId) container.kill(function (error) { if ( error != null && @@ -134,13 +133,13 @@ module.exports = DockerRunner = { error.message.match(/Cannot kill container .* is not running/) ) { logger.warn( - { err: error, container_id }, + { err: error, containerId }, 'container not running, continuing' ) error = null } if (error != null) { - logger.error({ err: error, container_id }, 'error killing container') + logger.error({ err: error, containerId }, 'error killing container') callback(error) } else { callback() @@ -424,7 +423,7 @@ module.exports = DockerRunner = { ) { if (error != null) { logger.error( - { err: error, container_id: containerId }, + { err: error, containerId }, 'error attaching to container' ) return attachStartCallback(error) @@ -432,7 +431,7 @@ module.exports = DockerRunner = { attachStartCallback() } - logger.log({ container_id: containerId }, 'attached to container') + logger.log({ containerId }, 'attached to container') const MAX_OUTPUT = 1024 * 1024 // limit output to 1MB const createStringOutputStream = function (name) { @@ -448,7 +447,7 @@ module.exports = DockerRunner = { } else { logger.error( { - container_id: containerId, + containerId, length: this.data.length, maxLen: MAX_OUTPUT }, @@ -470,7 +469,7 @@ module.exports = DockerRunner = { stream.on('error', (err) => logger.error( - { err, container_id: containerId }, + { err, containerId }, 'error reading from container stream' ) ) @@ -493,21 +492,15 @@ module.exports = DockerRunner = { let timedOut = false const timeoutId = setTimeout(function () { timedOut = true - logger.log( - { container_id: containerId }, - 'timeout reached, killing container' - ) + logger.log({ containerId }, 'timeout reached, killing container') container.kill(function () {}) }, timeout) - logger.log({ container_id: containerId }, 'waiting for docker container') + logger.log({ containerId }, 'waiting for docker container') container.wait(function (error, res) { if (error != null) { clearTimeout(timeoutId) - logger.error( - { err: error, container_id: containerId }, - 'error waiting for container' - ) + logger.error({ err: error, containerId }, 'error waiting for container') return callback(error) } if (timedOut) { @@ -518,7 +511,7 @@ module.exports = DockerRunner = { } else { clearTimeout(timeoutId) logger.log( - { container_id: containerId, exitCode: res.StatusCode }, + { containerId, exitCode: res.StatusCode }, 'docker container returned' ) callback(null, res.StatusCode) @@ -546,23 +539,20 @@ module.exports = DockerRunner = { }, _destroyContainer(containerId, shouldForce, callback) { - logger.log({ container_id: containerId }, 'destroying docker container') + logger.log({ containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) container.remove({ force: shouldForce === true }, function (error) { if (error != null && error.statusCode === 404) { logger.warn( - { err: error, container_id: containerId }, + { err: error, containerId }, 'container not found, continuing' ) error = null } if (error != null) { - logger.error( - { err: error, container_id: containerId }, - 'error destroying container' - ) + logger.error({ err: error, containerId }, 'error destroying container') } else { - logger.log({ container_id: containerId }, 'destroyed container') + logger.log({ containerId }, 'destroyed container') } callback(error) }) From 5b92439356919d6f297f2ed9c5363b07aa6ee425 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 15:05:13 -0400 Subject: [PATCH 575/709] Decaf cleanup: convert async function to sync The examineOldContainer() function doesn't need to use callbacks since it only does synchronous work. --- app/js/DockerRunner.js | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index f9977d80..cfe8d116 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -1,5 +1,4 @@ /* eslint-disable - handle-callback-err, no-return-assign, no-unused-vars, */ @@ -574,7 +573,7 @@ module.exports = DockerRunner = { { containerName: name, created, now, age, maxAge, ttl }, 'checking whether to destroy container' ) - callback(null, name, container.Id, ttl) + return { name, id: container.Id, ttl } }, destroyOldContainers(callback) { @@ -584,21 +583,15 @@ module.exports = DockerRunner = { } const jobs = [] for (const container of containers) { - DockerRunner.examineOldContainer(container, function ( - err, - name, - id, - ttl - ) { - if (name.slice(0, 9) === '/project-' && ttl <= 0) { - // strip the / prefix - // the LockManager uses the plain container name - name = name.slice(1) - jobs.push((cb) => - DockerRunner.destroyContainer(name, id, false, () => cb()) - ) - } - }) + const { name, id, ttl } = DockerRunner.examineOldContainer(container) + if (name.slice(0, 9) === '/project-' && ttl <= 0) { + // strip the / prefix + // the LockManager uses the plain container name + const plainName = name.slice(1) + jobs.push((cb) => + DockerRunner.destroyContainer(plainName, id, false, () => cb()) + ) + } } // Ignore errors because some containers get stuck but // will be destroyed next time From 1708e29faf190efd5d120c4aa903a76cb53c5b8f Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 15:30:07 -0400 Subject: [PATCH 576/709] Decaf cleanup: unused vars --- app/js/DockerRunner.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index cfe8d116..3e635cdb 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -1,8 +1,4 @@ -/* eslint-disable - no-return-assign, - no-unused-vars, -*/ -let DockerRunner, oneHour +let DockerRunner const Settings = require('settings-sharelatex') const logger = require('logger-sharelatex') const Docker = require('dockerode') @@ -14,6 +10,7 @@ const fs = require('fs') const Path = require('path') const _ = require('lodash') +const ONE_HOUR_IN_MS = 60 * 60 * 1000 logger.info('using docker runner') const usingSiblingContainers = () => @@ -559,8 +556,7 @@ module.exports = DockerRunner = { // handle expiry of docker containers - MAX_CONTAINER_AGE: - Settings.clsi.docker.maxContainerAge || (oneHour = 60 * 60 * 1000), + MAX_CONTAINER_AGE: Settings.clsi.docker.maxContainerAge || ONE_HOUR_IN_MS, examineOldContainer(container, callback) { const name = container.Name || (container.Names && container.Names[0]) @@ -618,7 +614,7 @@ module.exports = DockerRunner = { logger.error({ err }, 'failed to destroy old containers') } }), - (oneHour = 60 * 60 * 1000) + ONE_HOUR_IN_MS ) }, randomDelay) }, From c8371a8ea754875fba4a284e53637c5ef69c6e03 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 15:34:19 -0400 Subject: [PATCH 577/709] Do not instantiate errors at module load time This prevents the right stack trace from being captured. --- app/js/DockerRunner.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 3e635cdb..c422f572 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -22,11 +22,6 @@ let containerMonitorTimeout let containerMonitorInterval module.exports = DockerRunner = { - ERR_NOT_DIRECTORY: new Error('not a directory'), - ERR_TERMINATED: new Error('terminated'), - ERR_EXITED: new Error('exited'), - ERR_TIMED_OUT: new Error('container timed out'), - run( projectId, command, @@ -190,13 +185,13 @@ module.exports = DockerRunner = { } if (exitCode === 137) { // exit status from kill -9 - err = DockerRunner.ERR_TERMINATED + err = new Error('terminated') err.terminated = true return callback(err) } if (exitCode === 1) { // exit status from chktex - err = DockerRunner.ERR_EXITED + err = new Error('exited') err.code = exitCode return callback(err) } @@ -353,7 +348,7 @@ module.exports = DockerRunner = { return cb(err) } if (!stats.isDirectory()) { - return cb(DockerRunner.ERR_NOT_DIRECTORY) + return cb(new Error('not a directory')) } cb() }) @@ -501,7 +496,7 @@ module.exports = DockerRunner = { } if (timedOut) { logger.log({ containerId }, 'docker container timed out') - error = DockerRunner.ERR_TIMED_OUT + error = new Error('container timed out') error.timedout = true callback(error) } else { From faad50ff4ceb6d74dd26585734de7d9cc6922699 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 15:50:12 -0400 Subject: [PATCH 578/709] Decaf cleanup: simplify variable declarations --- app/js/DockerRunner.js | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index c422f572..f4e26598 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -1,4 +1,3 @@ -let DockerRunner const Settings = require('settings-sharelatex') const logger = require('logger-sharelatex') const Docker = require('dockerode') @@ -21,7 +20,7 @@ const usingSiblingContainers = () => let containerMonitorTimeout let containerMonitorInterval -module.exports = DockerRunner = { +const DockerRunner = { run( projectId, command, @@ -32,7 +31,6 @@ module.exports = DockerRunner = { compileGroup, callback ) { - let name if (usingSiblingContainers()) { const _newPath = Settings.path.sandboxedCompilesHostDir logger.log( @@ -49,14 +47,13 @@ module.exports = DockerRunner = { ) } - const volumes = {} - volumes[directory] = '/compile' + const volumes = { [directory]: '/compile' } command = command.map((arg) => arg.toString().replace('$COMPILE_DIR', '/compile') ) if (image == null) { - ;({ image } = Settings.clsi.docker) + image = Settings.clsi.docker.image } if ( @@ -80,7 +77,8 @@ module.exports = DockerRunner = { compileGroup ) const fingerprint = DockerRunner._fingerprintContainer(options) - options.name = name = `project-${projectId}-${fingerprint}` + const name = `project-${projectId}-${fingerprint}` + options.name = name // logOptions = _.clone(options) // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" @@ -179,19 +177,18 @@ module.exports = DockerRunner = { error, exitCode ) { - let err if (error != null) { return callback(error) } if (exitCode === 137) { // exit status from kill -9 - err = new Error('terminated') + const err = new Error('terminated') err.terminated = true return callback(err) } if (exitCode === 1) { // exit status from chktex - err = new Error('exited') + const err = new Error('exited') err.code = exitCode return callback(err) } @@ -199,7 +196,7 @@ module.exports = DockerRunner = { if (options != null && options.HostConfig != null) { options.HostConfig.SecurityOpt = null } - logger.log({ err, exitCode, options }, 'docker container has exited') + logger.log({ exitCode, options }, 'docker container has exited') callbackIfFinished() }) } @@ -214,13 +211,11 @@ module.exports = DockerRunner = { environment, compileGroup ) { - let m, year - let key, value, hostVol, dockerVol const timeoutInSeconds = timeout / 1000 const dockerVolumes = {} - for (hostVol in volumes) { - dockerVol = volumes[hostVol] + for (const hostVol in volumes) { + const dockerVol = volumes[hostVol] dockerVolumes[dockerVol] = {} if (volumes[hostVol].slice(-3).indexOf(':r') === -1) { @@ -231,17 +226,14 @@ module.exports = DockerRunner = { // merge settings and environment parameter const env = {} for (const src of [Settings.clsi.docker.env, environment || {}]) { - for (key in src) { - value = src[key] + for (const key in src) { + const value = src[key] env[key] = value } } // set the path based on the image year - if ((m = image.match(/:([0-9]+)\.[0-9]+/))) { - year = m[1] - } else { - year = '2014' - } + const match = image.match(/:([0-9]+)\.[0-9]+/) + const year = match ? match[1] : '2014' env.PATH = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/` const options = { Cmd: command, @@ -296,8 +288,7 @@ module.exports = DockerRunner = { Settings.clsi.docker.compileGroupConfig[compileGroup] ) { const override = Settings.clsi.docker.compileGroupConfig[compileGroup] - let key - for (key in override) { + for (const key in override) { _.set(options, key, override[key]) } } @@ -627,3 +618,5 @@ module.exports = DockerRunner = { } DockerRunner.startContainerMonitor() + +module.exports = DockerRunner From d9b25cdb180d74dd4de407841b7df9b02f20b5c0 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 15:50:45 -0400 Subject: [PATCH 579/709] Fix container monitor cleanup function The intent here is clearly to clear both the timeout and the interval. --- app/js/DockerRunner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index f4e26598..33000d7b 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -611,8 +611,8 @@ const DockerRunner = { containerMonitorTimeout = undefined } if (containerMonitorInterval) { - clearInterval(containerMonitorTimeout) - containerMonitorTimeout = undefined + clearInterval(containerMonitorInterval) + containerMonitorInterval = undefined } } } From c9590c8cfab0e8590562c83cf5d8c4e994246bbf Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 15:52:09 -0400 Subject: [PATCH 580/709] Use _.once() instead of ad hoc implementation --- app/js/DockerRunner.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 33000d7b..e355f950 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -137,12 +137,7 @@ const DockerRunner = { }, _runAndWaitForContainer(options, volumes, timeout, _callback) { - const callback = function (...args) { - _callback(...args) - // Only call the callback once - _callback = function () {} - } - + const callback = _.once(_callback) const { name } = options let streamEnded = false @@ -463,11 +458,7 @@ const DockerRunner = { }, waitForContainer(containerId, timeout, _callback) { - const callback = function (...args) { - _callback(...args) - // Only call the callback once - _callback = function () {} - } + const callback = _.once(_callback) const container = dockerode.getContainer(containerId) From f4fb979c63d7edd65a5de31cb1d7bb4ddd5fc9e7 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 15:58:16 -0400 Subject: [PATCH 581/709] Decaf cleanup: normalize functions Use function keyword for declarations and arrow functions for callbacks. --- app/js/DockerRunner.js | 109 +++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index e355f950..d357a241 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -12,10 +12,13 @@ const _ = require('lodash') const ONE_HOUR_IN_MS = 60 * 60 * 1000 logger.info('using docker runner') -const usingSiblingContainers = () => - Settings != null && - Settings.path != null && - Settings.path.sandboxedCompilesHostDir != null +function usingSiblingContainers() { + return ( + Settings != null && + Settings.path != null && + Settings.path.sandboxedCompilesHostDir != null + ) +} let containerMonitorTimeout let containerMonitorInterval @@ -83,30 +86,32 @@ const DockerRunner = { // logOptions = _.clone(options) // logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging" logger.log({ projectId }, 'running docker container') - DockerRunner._runAndWaitForContainer(options, volumes, timeout, function ( - error, - output - ) { - if (error && error.statusCode === 500) { - logger.log( - { err: error, projectId }, - 'error running container so destroying and retrying' - ) - DockerRunner.destroyContainer(name, null, true, function (error) { - if (error != null) { - return callback(error) - } - DockerRunner._runAndWaitForContainer( - options, - volumes, - timeout, - callback + DockerRunner._runAndWaitForContainer( + options, + volumes, + timeout, + (error, output) => { + if (error && error.statusCode === 500) { + logger.log( + { err: error, projectId }, + 'error running container so destroying and retrying' ) - }) - } else { - callback(error, output) + DockerRunner.destroyContainer(name, null, true, (error) => { + if (error != null) { + return callback(error) + } + DockerRunner._runAndWaitForContainer( + options, + volumes, + timeout, + callback + ) + }) + } else { + callback(error, output) + } } - }) + ) // pass back the container name to allow it to be killed return name @@ -115,7 +120,7 @@ const DockerRunner = { kill(containerId, callback) { logger.log({ containerId }, 'sending kill signal to container') const container = dockerode.getContainer(containerId) - container.kill(function (error) { + container.kill((error) => { if ( error != null && error.message != null && @@ -144,13 +149,13 @@ const DockerRunner = { let containerReturned = false let output = {} - const callbackIfFinished = function () { + function callbackIfFinished() { if (streamEnded && containerReturned) { callback(null, output) } } - const attachStreamHandler = function (error, _output) { + function attachStreamHandler(error, _output) { if (error != null) { return callback(error) } @@ -163,15 +168,12 @@ const DockerRunner = { options, volumes, attachStreamHandler, - function (error, containerId) { + (error, containerId) => { if (error != null) { return callback(error) } - DockerRunner.waitForContainer(name, timeout, function ( - error, - exitCode - ) { + DockerRunner.waitForContainer(name, timeout, (error, exitCode) => { if (error != null) { return callback(error) } @@ -305,7 +307,7 @@ const DockerRunner = { // When a container is started with volume pointing to a // non-existent directory then docker creates the directory but // with root ownership. - DockerRunner._checkVolumes(options, volumes, function (err) { + DockerRunner._checkVolumes(options, volumes, (err) => { if (err != null) { return releaseLock(err) } @@ -329,7 +331,7 @@ const DockerRunner = { } const checkVolume = (path, cb) => - fs.stat(path, function (err, stats) { + fs.stat(path, (err, stats) => { if (err != null) { return cb(err) } @@ -352,22 +354,24 @@ const DockerRunner = { logger.log({ container_name: name }, 'starting container') const container = dockerode.getContainer(name) - const createAndStartContainer = () => - dockerode.createContainer(options, function (error, container) { + function createAndStartContainer() { + dockerode.createContainer(options, (error, container) => { if (error != null) { return callback(error) } startExistingContainer() }) - var startExistingContainer = () => + } + + function startExistingContainer() { DockerRunner.attachToContainer( options.name, attachStreamHandler, - function (error) { + (error) => { if (error != null) { return callback(error) } - container.start(function (error) { + container.start((error) => { if (error != null && error.statusCode !== 304) { callback(error) } else { @@ -377,7 +381,9 @@ const DockerRunner = { }) } ) - container.inspect(function (error, stats) { + } + + container.inspect((error, stats) => { if (error != null && error.statusCode === 404) { createAndStartContainer() } else if (error != null) { @@ -394,10 +400,7 @@ const DockerRunner = { attachToContainer(containerId, attachStreamHandler, attachStartCallback) { const container = dockerode.getContainer(containerId) - container.attach({ stdout: 1, stderr: 1, stream: 1 }, function ( - error, - stream - ) { + container.attach({ stdout: 1, stderr: 1, stream: 1 }, (error, stream) => { if (error != null) { logger.error( { err: error, containerId }, @@ -411,7 +414,7 @@ const DockerRunner = { logger.log({ containerId }, 'attached to container') const MAX_OUTPUT = 1024 * 1024 // limit output to 1MB - const createStringOutputStream = function (name) { + function createStringOutputStream(name) { return { data: '', overflowed: false, @@ -463,14 +466,16 @@ const DockerRunner = { const container = dockerode.getContainer(containerId) let timedOut = false - const timeoutId = setTimeout(function () { + const timeoutId = setTimeout(() => { timedOut = true logger.log({ containerId }, 'timeout reached, killing container') - container.kill(function () {}) + container.kill((err) => { + logger.warn({ err, containerId }, 'failed to kill container') + }) }, timeout) logger.log({ containerId }, 'waiting for docker container') - container.wait(function (error, res) { + container.wait((error, res) => { if (error != null) { clearTimeout(timeoutId) logger.error({ err: error, containerId }, 'error waiting for container') @@ -514,7 +519,7 @@ const DockerRunner = { _destroyContainer(containerId, shouldForce, callback) { logger.log({ containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) - container.remove({ force: shouldForce === true }, function (error) { + container.remove({ force: shouldForce === true }, (error) => { if (error != null && error.statusCode === 404) { logger.warn( { err: error, containerId }, @@ -550,7 +555,7 @@ const DockerRunner = { }, destroyOldContainers(callback) { - dockerode.listContainers({ all: true }, function (error, containers) { + dockerode.listContainers({ all: true }, (error, containers) => { if (error != null) { return callback(error) } From c769ae63a2afd891bca6668d53f7df82d379c64c Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Thu, 3 Sep 2020 17:10:24 -0400 Subject: [PATCH 582/709] Mount /home/tex in an anonymous volume When we mount the container's root filesystem as read-only, mount an anonymous volume in /home/tex so that it's writable. Our TeX Live images have cached content in /home/tex. This content will automatically get copied by Docker into this anonymous volume. --- app/js/DockerRunner.js | 3 ++- test/unit/js/DockerRunnerTests.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index d357a241..0ff81095 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -277,6 +277,7 @@ const DockerRunner = { if (Settings.clsi.docker.Readonly) { options.HostConfig.ReadonlyRootfs = true options.HostConfig.Tmpfs = { '/tmp': 'rw,noexec,nosuid,size=65536k' } + options.Volumes['/home/tex'] = {} } // Allow per-compile group overriding of individual settings @@ -519,7 +520,7 @@ const DockerRunner = { _destroyContainer(containerId, shouldForce, callback) { logger.log({ containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) - container.remove({ force: shouldForce === true }, (error) => { + container.remove({ force: shouldForce === true, v: true }, (error) => { if (error != null && error.statusCode === 404) { logger.warn( { err: error, containerId }, diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 553b20f3..ac68c750 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -802,7 +802,7 @@ describe('DockerRunner', function () { (err) => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove - .calledWith({ force: true }) + .calledWithMatch({ force: true }) .should.equal(true) return done() } @@ -816,7 +816,7 @@ describe('DockerRunner', function () { (err) => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove - .calledWith({ force: false }) + .calledWithMatch({ force: false }) .should.equal(true) return done() } From 0a05fa7abc76efb245a07ccb4dde8ff5cc8241d7 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Fri, 4 Sep 2020 11:34:08 -0400 Subject: [PATCH 583/709] Test anonymous volumes are removed with containers --- test/unit/js/DockerRunnerTests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index ac68c750..517179a9 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -802,7 +802,7 @@ describe('DockerRunner', function () { (err) => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove - .calledWithMatch({ force: true }) + .calledWith({ force: true, v: true }) .should.equal(true) return done() } @@ -816,7 +816,7 @@ describe('DockerRunner', function () { (err) => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove - .calledWithMatch({ force: false }) + .calledWith({ force: false, v: true }) .should.equal(true) return done() } From 610b04a738f0e9f1f7420147ea569e57af4819b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2020 17:04:49 +0000 Subject: [PATCH 584/709] Bump node-fetch from 2.6.0 to 2.6.1 Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1. - [Release notes](https://github.com/bitinn/node-fetch/releases) - [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1) Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e185ad12..170c1ac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4662,9 +4662,9 @@ } }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-forge": { "version": "0.9.1", From 1650a618316ae1f5bff6075f8e96ab557d4cc7fc Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Wed, 16 Sep 2020 12:24:42 -0400 Subject: [PATCH 585/709] Bump Node version to 10.22.1 --- .github/dependabot.yml | 6 ++++++ .nvmrc | 2 +- Dockerfile | 2 +- buildscript.txt | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c6f98d84..e2c64a33 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,3 +15,9 @@ updates: # Block informal upgrades -- security upgrades use a separate queue. # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#open-pull-requests-limit open-pull-requests-limit: 0 + + # currently assign team-magma to all dependabot PRs - this may change in + # future if we reorganise teams + labels: + - "dependencies" + - "Team-Magma" diff --git a/.nvmrc b/.nvmrc index b61c07ff..c2f64213 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.21.0 +10.22.1 diff --git a/Dockerfile b/Dockerfile index bbf1efd0..5d702dce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -FROM node:10.21.0 as base +FROM node:10.22.1 as base WORKDIR /app COPY install_deps.sh /app diff --git a/buildscript.txt b/buildscript.txt index b2866578..0d616441 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -4,6 +4,6 @@ clsi --docker-repos=gcr.io/overleaf-ops --env-add= --env-pass-through=TEXLIVE_IMAGE ---node-version=10.21.0 +--node-version=10.22.1 --public-repo=True ---script-version=3.3.2 +--script-version=3.3.3 From 4ed620d0e113f158e1d59fd5435f127648586788 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane.kilkelly@overleaf.com> Date: Tue, 27 Oct 2020 15:53:10 +0000 Subject: [PATCH 586/709] In example request, show example using curl This clarifies the exact way to construct this request. It seems that some users have taken the existing documentation to mean that the request should be sent from the browser, to the web server. This example clarifies how to send this compile request directly to the clsi server. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5ae02a86..492f2fdb 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,16 @@ The CLSI is based on a JSON API. } ``` +With `curl`, if you place the above json in a file called `data.json`, the request would look like this: + +``` shell +$ curl -X POST -d @data.json localhost:3013/project/<id>/compile +``` + You can specify any project-id in the URL, and the files and LaTeX environment will be persisted between requests. URLs will be downloaded and cached until provided with a more recent modified date. + #### Example Response ```javascript From 8c3aec1c8f4b3131fce5815faabde6e9bdc53954 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane.kilkelly@overleaf.com> Date: Tue, 27 Oct 2020 16:17:22 +0000 Subject: [PATCH 587/709] No more blank line --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 492f2fdb..02cd0a47 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,6 @@ $ curl -X POST -d @data.json localhost:3013/project/<id>/compile You can specify any project-id in the URL, and the files and LaTeX environment will be persisted between requests. URLs will be downloaded and cached until provided with a more recent modified date. - #### Example Response ```javascript From 7ea39504f60b4c9fd1440fcd42bad38771ed97bf Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Mon, 23 Nov 2020 10:56:33 -0500 Subject: [PATCH 588/709] Upgrade build-scripts to 3.4.0 This version fixes docker-compose health checks for dependent services. See https://github.com/overleaf/dev-environment/pull/409 for details. --- buildscript.txt | 2 +- docker-compose.ci.yml | 1 + docker-compose.yml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/buildscript.txt b/buildscript.txt index 0d616441..6794c8cb 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -6,4 +6,4 @@ clsi --env-pass-through=TEXLIVE_IMAGE --node-version=10.22.1 --public-repo=True ---script-version=3.3.3 +--script-version=3.4.0 diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 35f48780..22379337 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -22,6 +22,7 @@ services: environment: ELASTIC_SEARCH_DSN: es:9200 REDIS_HOST: redis + QUEUES_REDIS_HOST: redis MONGO_HOST: mongo POSTGRES_HOST: postgres MOCHA_GREP: ${MOCHA_GREP} diff --git a/docker-compose.yml b/docker-compose.yml index 052d9098..66880002 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: environment: ELASTIC_SEARCH_DSN: es:9200 REDIS_HOST: redis + QUEUES_REDIS_HOST: redis MONGO_HOST: mongo POSTGRES_HOST: postgres MOCHA_GREP: ${MOCHA_GREP} From 17a164e4e0dc671d168069fab5ed7276776f8b3a Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 25 Nov 2020 11:57:25 +0000 Subject: [PATCH 589/709] [misc] bump metrics module to 3.4.1 - renamed package from `metrics-sharelatex` to `@overleaf/metrics` - drop support for statsd backend - decaffeinate - compress `/metrics` response using gzip - bump debugging agents to latest versions - expose prometheus interfaces for custom metrics (custom tags) - cleanup of open sockets metrics - fix deprecation warnings for header access --- app.js | 2 +- app/js/Metrics.js | 2 +- package-lock.json | 1238 ++++++++++++++++++++------------------------- package.json | 3 +- 4 files changed, 539 insertions(+), 706 deletions(-) diff --git a/app.js b/app.js index 7ebca04e..9402869c 100644 --- a/app.js +++ b/app.js @@ -6,7 +6,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const tenMinutes = 10 * 60 * 1000 -const Metrics = require('metrics-sharelatex') +const Metrics = require('@overleaf/metrics') Metrics.initialize('clsi') const CompileController = require('./app/js/CompileController') diff --git a/app/js/Metrics.js b/app/js/Metrics.js index e9676415..f0e57794 100644 --- a/app/js/Metrics.js +++ b/app/js/Metrics.js @@ -1,3 +1,3 @@ // TODO: This file was created by bulk-decaffeinate. // Sanity-check the conversion and remove this comment. -module.exports = require('metrics-sharelatex') +module.exports = require('@overleaf/metrics') diff --git a/package-lock.json b/package-lock.json index 31171ac4..23e4179f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -215,173 +215,6 @@ } } }, - "@google-cloud/debug-agent": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-3.2.0.tgz", - "integrity": "sha512-fP87kYbS6aeDna08BivwQ1J260mwJGchRi99XdWCgqbRwuFac8ul0OT5i2wEeDSc5QaDX8ZuWQQ0igZvh1rTyQ==", - "requires": { - "@google-cloud/common": "^0.32.0", - "@sindresorhus/is": "^0.15.0", - "acorn": "^6.0.0", - "coffeescript": "^2.0.0", - "console-log-level": "^1.4.0", - "extend": "^3.0.1", - "findit2": "^2.2.3", - "gcp-metadata": "^1.0.0", - "lodash.pickby": "^4.6.0", - "p-limit": "^2.2.0", - "pify": "^4.0.1", - "semver": "^6.0.0", - "source-map": "^0.6.1", - "split": "^1.0.0" - }, - "dependencies": { - "@google-cloud/common": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz", - "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==", - "requires": { - "@google-cloud/projectify": "^0.3.3", - "@google-cloud/promisify": "^0.4.0", - "@types/request": "^2.48.1", - "arrify": "^2.0.0", - "duplexify": "^3.6.0", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^3.1.1", - "pify": "^4.0.1", - "retry-request": "^4.0.0", - "teeny-request": "^3.11.3" - } - }, - "@google-cloud/projectify": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", - "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" - }, - "@google-cloud/promisify": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz", - "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==" - }, - "coffeescript": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", - "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "gaxios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", - "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.3.0" - } - }, - "gcp-metadata": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz", - "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==", - "requires": { - "gaxios": "^1.0.2", - "json-bigint": "^0.3.0" - } - }, - "google-auth-library": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz", - "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==", - "requires": { - "base64-js": "^1.3.0", - "fast-text-encoding": "^1.0.0", - "gaxios": "^1.2.1", - "gcp-metadata": "^1.0.0", - "gtoken": "^2.3.2", - "https-proxy-agent": "^2.2.1", - "jws": "^3.1.5", - "lru-cache": "^5.0.0", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "google-p12-pem": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", - "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", - "requires": { - "node-forge": "^0.8.0", - "pify": "^4.0.0" - } - }, - "gtoken": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", - "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", - "requires": { - "gaxios": "^1.0.4", - "google-p12-pem": "^1.0.0", - "jws": "^3.1.5", - "mime": "^2.2.0", - "pify": "^4.0.0" - } - }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - } - }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node-forge": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", - "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "teeny-request": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", - "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", - "requires": { - "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.2.0", - "uuid": "^3.3.2" - } - } - } - }, "@google-cloud/logging": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-7.3.0.tgz", @@ -468,316 +301,336 @@ "extend": "^3.0.2" } }, - "@google-cloud/profiler": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-0.2.3.tgz", - "integrity": "sha512-rNvtrFtIebIxZEJ/O0t8n7HciZGIXBo8DvHxWqAmsCaeLvkTtsaL6HmPkwxrNQ1IhbYWAxF+E/DwCiHyhKmgTg==", + "@google-cloud/projectify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", + "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==" + }, + "@google-cloud/promisify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", + "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==" + }, + "@grpc/grpc-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", + "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", "requires": { - "@google-cloud/common": "^0.26.0", - "@types/console-log-level": "^1.4.0", - "@types/semver": "^5.5.0", - "bindings": "^1.2.1", - "console-log-level": "^1.4.0", - "delay": "^4.0.1", - "extend": "^3.0.1", - "gcp-metadata": "^0.9.0", - "nan": "^2.11.1", - "parse-duration": "^0.1.1", - "pify": "^4.0.0", - "pretty-ms": "^4.0.0", - "protobufjs": "~6.8.6", - "semver": "^5.5.0", - "teeny-request": "^3.3.0" + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@grpc/proto-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", + "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", + "requires": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + } + }, + "@opencensus/core": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.20.tgz", + "integrity": "sha512-vqOuTd2yuMpKohp8TNNGUAPjWEGjlnGfB9Rh5e3DKqeyR94YgierNs4LbMqxKtsnwB8Dm2yoEtRuUgoe5vD9DA==", + "requires": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^6.0.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@opencensus/propagation-stackdriver": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.20.tgz", + "integrity": "sha512-P8yuHSLtce+yb+2EZjtTVqG7DQ48laC+IuOWi3X9q78s1Gni5F9+hmbmyP6Nb61jb5BEvXQX1s2rtRI6bayUWA==", + "requires": { + "@opencensus/core": "^0.0.20", + "hex2dec": "^1.0.1", + "uuid": "^3.2.1" + } + }, + "@overleaf/metrics": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@overleaf/metrics/-/metrics-3.4.1.tgz", + "integrity": "sha512-OgjlzuC+2gPdIEDHhmd9LDMu01tk1ln0cJhw1727BZ+Wgf2Z1hjuHRt4JeCkf+PFTHwJutVYT8v6IGPpNEPtbg==", + "requires": { + "@google-cloud/debug-agent": "^5.1.2", + "@google-cloud/profiler": "^4.0.3", + "@google-cloud/trace-agent": "^5.1.1", + "compression": "^1.7.4", + "prom-client": "^11.1.3", + "underscore": "~1.6.0", + "yn": "^3.1.1" }, "dependencies": { "@google-cloud/common": { - "version": "0.26.2", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.26.2.tgz", - "integrity": "sha512-xJ2M/q3MrUbnYZuFlpF01caAlEhAUoRn0NXp93Hn3pkFpfSOG8YfbKbpBAHvcKVbBOAKVIwPsleNtuyuabUwLQ==", - "requires": { - "@google-cloud/projectify": "^0.3.2", - "@google-cloud/promisify": "^0.3.0", - "@types/duplexify": "^3.5.0", - "@types/request": "^2.47.0", - "arrify": "^1.0.1", - "duplexify": "^3.6.0", - "ent": "^2.2.0", - "extend": "^3.0.1", - "google-auth-library": "^2.0.0", - "pify": "^4.0.0", - "retry-request": "^4.0.0", - "through2": "^3.0.0" - } - }, - "@google-cloud/projectify": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", - "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" - }, - "@google-cloud/promisify": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.3.1.tgz", - "integrity": "sha512-QzB0/IMvB0eFxFK7Eqh+bfC8NLv3E9ScjWQrPOk6GgfNroxcVITdTlT8NRsRrcp5+QQJVPLkRqKG0PUdaWXmHw==" - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz", + "integrity": "sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==", "requires": { - "ms": "^2.1.1" + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^6.1.1", + "retry-request": "^4.1.1", + "teeny-request": "^7.0.0" } }, - "gaxios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", - "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", + "@google-cloud/debug-agent": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-5.1.3.tgz", + "integrity": "sha512-WbzeEz4MvPlM7DX2QBsPcWgF62u7LSQv/oMYPl0L+TddTebqjDKiVXwxpzWk61NIfcKiet3dyCbPIt3N5o8XPQ==", "requires": { - "abort-controller": "^3.0.0", + "@google-cloud/common": "^3.0.0", + "acorn": "^8.0.0", + "coffeescript": "^2.0.0", + "console-log-level": "^1.4.0", "extend": "^3.0.2", - "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.3.0" + "findit2": "^2.2.3", + "gcp-metadata": "^4.0.0", + "p-limit": "^3.0.1", + "semver": "^7.0.0", + "source-map": "^0.6.1", + "split": "^1.0.0" } }, - "gcp-metadata": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.9.3.tgz", - "integrity": "sha512-caV4S84xAjENtpezLCT/GILEAF5h/bC4cNqZFmt/tjTn8t+JBtTkQrgBrJu3857YdsnlM8rxX/PMcKGtE8hUlw==", - "requires": { - "gaxios": "^1.0.2", - "json-bigint": "^0.3.0" + "@google-cloud/profiler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-4.1.0.tgz", + "integrity": "sha512-9e1zXRctLSUHAoAsFGwE4rS28fr0siiG+jXl5OpwTK8ZAUlxb70aosHaZGdsv8YXrYKjuiufjRZ/OXCs0XLI9g==", + "requires": { + "@google-cloud/common": "^3.0.0", + "@types/console-log-level": "^1.4.0", + "@types/semver": "^7.0.0", + "console-log-level": "^1.4.0", + "delay": "^4.0.1", + "extend": "^3.0.2", + "gcp-metadata": "^4.0.0", + "parse-duration": "^0.4.4", + "pprof": "3.0.0", + "pretty-ms": "^7.0.0", + "protobufjs": "~6.10.0", + "semver": "^7.0.0", + "teeny-request": "^7.0.0" } }, - "google-auth-library": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-2.0.2.tgz", - "integrity": "sha512-FURxmo1hBVmcfLauuMRKOPYAPKht3dGuI2wjeJFalDUThO0HoYVjr4yxt5cgYSFm1dgUpmN9G/poa7ceTFAIiA==", - "requires": { - "axios": "^0.18.0", - "gcp-metadata": "^0.7.0", - "gtoken": "^2.3.0", - "https-proxy-agent": "^2.2.1", - "jws": "^3.1.5", - "lru-cache": "^5.0.0", - "semver": "^5.5.0" - }, - "dependencies": { - "gcp-metadata": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.7.0.tgz", - "integrity": "sha512-ffjC09amcDWjh3VZdkDngIo7WoluyC5Ag9PAYxZbmQLOLNI8lvPtoKTSCyU54j2gwy5roZh6sSMTfkY2ct7K3g==", - "requires": { - "axios": "^0.18.0", - "extend": "^3.0.1", - "retry-axios": "0.3.2" - } - } - } + "@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==" }, - "google-p12-pem": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", - "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", - "requires": { - "node-forge": "^0.8.0", - "pify": "^4.0.0" + "@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" + }, + "@google-cloud/trace-agent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-5.1.1.tgz", + "integrity": "sha512-YTcK0RLN90pLCprg0XC8uV4oAVd79vsXhkcxmEVwiOOYjUDvSrAhb7y/0SY606zgfhJHmUTNb/fZSWEtZP/slQ==", + "requires": { + "@google-cloud/common": "^3.0.0", + "@opencensus/propagation-stackdriver": "0.0.22", + "builtin-modules": "^3.0.0", + "console-log-level": "^1.4.0", + "continuation-local-storage": "^3.2.1", + "extend": "^3.0.2", + "gcp-metadata": "^4.0.0", + "google-auth-library": "^6.0.0", + "hex2dec": "^1.0.1", + "is": "^3.2.0", + "methods": "^1.1.1", + "require-in-the-middle": "^5.0.0", + "semver": "^7.0.0", + "shimmer": "^1.2.0", + "source-map-support": "^0.5.16", + "uuid": "^8.0.0" } }, - "gtoken": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", - "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", + "@opencensus/core": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.22.tgz", + "integrity": "sha512-ErazJtivjceNoOZI1bG9giQ6cWS45J4i6iPUtlp7dLNu58OLs/v+CD0FsaPCh47XgPxAI12vbBE8Ec09ViwHNA==", "requires": { - "gaxios": "^1.0.4", - "google-p12-pem": "^1.0.0", - "jws": "^3.1.5", - "mime": "^2.2.0", - "pify": "^4.0.0" + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^7.0.0", + "shimmer": "^1.2.0", + "uuid": "^8.0.0" } }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "@opencensus/propagation-stackdriver": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.22.tgz", + "integrity": "sha512-eBvf/ihb1mN8Yz/ASkz8nHzuMKqygu77+VNnUeR0yEh3Nj+ykB8VVR6lK+NAFXo1Rd1cOsTmgvuXAZgDAGleQQ==", "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "@opencensus/core": "^0.0.22", + "hex2dec": "^1.0.1", + "uuid": "^8.0.0" } }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "@types/node": { + "version": "13.13.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.33.tgz", + "integrity": "sha512-1B3GM1yuYsFyEvBb+ljBqWBOylsWDYioZ5wpu8AhXdIhq20neXS7eaSC8GkwHE0yQYGiOIV43lMsgRYTgKZefQ==" }, - "node-forge": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", - "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" + "@types/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==" }, - "teeny-request": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", - "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", - "requires": { - "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.2.0", - "uuid": "^3.3.2" - } - } - } - }, - "@google-cloud/projectify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", - "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==" - }, - "@google-cloud/promisify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", - "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==" - }, - "@google-cloud/trace-agent": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-3.6.1.tgz", - "integrity": "sha512-KDo85aPN4gSxJ7oEIOlKd7aGENZFXAM1kbIn1Ds+61gh/K1CQWSyepgJo3nUpAwH6D1ezDWV7Iaf8ueoITc8Uw==", - "requires": { - "@google-cloud/common": "^0.32.1", - "builtin-modules": "^3.0.0", - "console-log-level": "^1.4.0", - "continuation-local-storage": "^3.2.1", - "extend": "^3.0.0", - "gcp-metadata": "^1.0.0", - "hex2dec": "^1.0.1", - "is": "^3.2.0", - "methods": "^1.1.1", - "require-in-the-middle": "^4.0.0", - "semver": "^6.0.0", - "shimmer": "^1.2.0", - "uuid": "^3.0.1" - }, - "dependencies": { - "@google-cloud/common": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz", - "integrity": "sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ==", - "requires": { - "@google-cloud/projectify": "^0.3.3", - "@google-cloud/promisify": "^0.4.0", - "@types/request": "^2.48.1", - "arrify": "^2.0.0", - "duplexify": "^3.6.0", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^3.1.1", - "pify": "^4.0.1", - "retry-request": "^4.0.0", - "teeny-request": "^3.11.3" - } + "acorn": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz", + "integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==" }, - "@google-cloud/projectify": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz", - "integrity": "sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw==" + "bignumber.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" }, - "@google-cloud/promisify": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz", - "integrity": "sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q==" + "coffeescript": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", + "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + } + }, + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" } }, "gaxios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", - "integrity": "sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.0.1.tgz", + "integrity": "sha512-jOin8xRZ/UytQeBpSXFqIzqU7Fi5TqgPNLlUsSB8kjJ76+FiGBfImF8KJu++c6J4jOldfJUtt0YmkRj2ZpSHTQ==", "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", - "https-proxy-agent": "^2.2.1", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", "node-fetch": "^2.3.0" } }, "gcp-metadata": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz", - "integrity": "sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", + "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", "requires": { - "gaxios": "^1.0.2", - "json-bigint": "^0.3.0" + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" } }, "google-auth-library": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz", - "integrity": "sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.3.tgz", + "integrity": "sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g==", "requires": { + "arrify": "^2.0.0", "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^1.2.1", - "gcp-metadata": "^1.0.0", - "gtoken": "^2.3.2", - "https-proxy-agent": "^2.2.1", - "jws": "^3.1.5", - "lru-cache": "^5.0.0", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" } }, "google-p12-pem": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", - "integrity": "sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", + "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", "requires": { - "node-forge": "^0.8.0", - "pify": "^4.0.0" + "node-forge": "^0.10.0" } }, "gtoken": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", - "integrity": "sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.1.0.tgz", + "integrity": "sha512-4d8N6Lk8TEAHl9vVoRVMh9BNOKWVgl2DdNtr3428O75r3QFrF/a5MMu851VmK0AA8+iSvbwRv69k5XnMLURGhg==", "requires": { - "gaxios": "^1.0.4", - "google-p12-pem": "^1.0.0", - "jws": "^3.1.5", - "mime": "^2.2.0", - "pify": "^4.0.0" + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0", + "mime": "^2.2.0" + } + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" } }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" }, "ms": { "version": "2.1.2", @@ -785,80 +638,100 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node-forge": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", - "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, - "teeny-request": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", - "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "requires": { - "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.2.0", - "uuid": "^3.3.2" + "yocto-queue": "^0.1.0" } - } - } - }, - "@grpc/grpc-js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", - "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", - "requires": { - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@grpc/proto-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", - "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", - "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - } - }, - "@opencensus/core": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.20.tgz", - "integrity": "sha512-vqOuTd2yuMpKohp8TNNGUAPjWEGjlnGfB9Rh5e3DKqeyR94YgierNs4LbMqxKtsnwB8Dm2yoEtRuUgoe5vD9DA==", - "requires": { - "continuation-local-storage": "^3.2.1", - "log-driver": "^1.2.7", - "semver": "^6.0.0", - "shimmer": "^1.2.0", - "uuid": "^3.2.1" - }, - "dependencies": { + }, + "parse-duration": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.4.4.tgz", + "integrity": "sha512-KbAJuYGUhZkB9gotDiKLnZ7Z3VTacK3fgwmDdB6ZVDtJbMBT6MfLga0WJaYpPDu0mzqT0NgHtHDt5PY4l0nidg==" + }, + "pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "requires": { + "parse-ms": "^2.1.0" + } + }, + "protobufjs": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", + "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "require-in-the-middle": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.0.3.tgz", + "integrity": "sha512-p/ICV8uMlqC4tjOYabLMxAWCIKa0YUQgZZ6KDM0xgXJNgdGQ1WmL2A07TwmrZw+wi6ITUFKzH5v3n+ENEyXVkA==", + "requires": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.12.0" + } + }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "teeny-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", + "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + } + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, - "@opencensus/propagation-stackdriver": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.20.tgz", - "integrity": "sha512-P8yuHSLtce+yb+2EZjtTVqG7DQ48laC+IuOWi3X9q78s1Gni5F9+hmbmyP6Nb61jb5BEvXQX1s2rtRI6bayUWA==", - "requires": { - "@opencensus/core": "^0.0.20", - "hex2dec": "^1.0.1", - "uuid": "^3.2.1" - } - }, "@overleaf/o-error": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-3.0.0.tgz", @@ -918,11 +791,6 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, - "@sindresorhus/is": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.15.0.tgz", - "integrity": "sha512-lu8BpxjAtRCAo5ifytTpCPCj99LF7o/2Myn+NXyNCBqvPYn7Pjd76AMmUB5l7XF1U6t0hcWrlEM5ESufW7wAeA==" - }, "@sinonjs/commons": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", @@ -989,11 +857,6 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -1005,14 +868,6 @@ "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz", "integrity": "sha512-x+OscEQwcx5Biair4enH7ov9W+clcqUWaZRaxn5IkT4yNWWjRr2oiYDkY/x1uXSTVZOQ2xlbFQySaQGB+VdXGQ==" }, - "@types/duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", - "requires": { - "@types/node": "*" - } - }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -1043,39 +898,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz", "integrity": "sha1-IOhWUbYv2GZW5Xycm8dxqxVwvFk=" }, - "@types/request": { - "version": "2.48.4", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.4.tgz", - "integrity": "sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw==", - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } - } - }, - "@types/semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==" - }, - "@types/tough-cookie": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz", - "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==" - }, "@typescript-eslint/experimental-utils": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", @@ -1154,7 +976,8 @@ "acorn": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true }, "acorn-jsx": { "version": "5.1.0", @@ -1162,14 +985,6 @@ "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, "ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", @@ -1365,15 +1180,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, - "axios": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", - "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", - "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" - } - }, "axobject-query": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz", @@ -1788,6 +1594,35 @@ "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", "dev": true }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2200,14 +2035,6 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2937,24 +2764,6 @@ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -3776,7 +3585,8 @@ "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true }, "is-callable": { "version": "1.1.5", @@ -3976,25 +3786,6 @@ "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", "dev": true }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -4087,11 +3878,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.pickby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", - "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=" - }, "lodash.unescape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", @@ -4208,15 +3994,6 @@ "yallist": "^3.0.2" } }, - "lynx": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.2.0.tgz", - "integrity": "sha1-eeZnRTDaQYPoeVO9aGFx4HDaULk=", - "requires": { - "mersenne": "~0.0.3", - "statsd-parser": "~0.0.4" - } - }, "make-plural": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz", @@ -4267,11 +4044,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, - "mersenne": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", - "integrity": "sha1-QB/ex+whzbngPNPTAhOY2iGycIU=" - }, "messageformat": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz", @@ -4300,37 +4072,6 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, - "metrics-sharelatex": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/metrics-sharelatex/-/metrics-sharelatex-2.6.0.tgz", - "integrity": "sha512-kPWCtgBrRZwLXCxqJVVn3c7g+GHQEBGYBpwCIt0Vqp0NaKvgKiPkJMkoPg9vkCsjsN2AgpGxXcOAdnHAjxfrzA==", - "requires": { - "@google-cloud/debug-agent": "^3.0.0", - "@google-cloud/profiler": "^0.2.3", - "@google-cloud/trace-agent": "^3.2.0", - "coffee-script": "1.6.0", - "lynx": "~0.1.1", - "prom-client": "^11.1.3", - "underscore": "~1.6.0", - "yn": "^3.1.1" - }, - "dependencies": { - "lynx": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", - "integrity": "sha1-Mxjc7xaQi4KG6Bisz9sxzXQkj50=", - "requires": { - "mersenne": "~0.0.3", - "statsd-parser": "~0.0.4" - } - }, - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" - } - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -4854,6 +4595,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4908,6 +4654,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, "requires": { "p-try": "^2.0.0" } @@ -4941,7 +4688,8 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true }, "parent-module": { "version": "1.0.1", @@ -4952,11 +4700,6 @@ "callsites": "^3.0.0" } }, - "parse-duration": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.2.tgz", - "integrity": "sha512-0qfMZyjOUFBeEIvJ5EayfXJqaEXxQ+Oj2b7tWJM3hvEXvXsYCk05EDVI23oYnEw2NaFYUWdABEVPBvBMh8L/pA==" - }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -5043,11 +4786,6 @@ "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", "dev": true }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, "pkg-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", @@ -5057,6 +4795,126 @@ "find-up": "^2.1.0" } }, + "pprof": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pprof/-/pprof-3.0.0.tgz", + "integrity": "sha512-uPWbAhoH/zvq1kM3/Fd/wshb4D7sLlGap8t6uCTER4aZRWqqyPYgXzpjWbT0Unn5U25pEy2VREUu27nQ9o9VPA==", + "requires": { + "bindings": "^1.2.1", + "delay": "^4.0.1", + "findit2": "^2.2.3", + "nan": "^2.14.0", + "node-pre-gyp": "^0.16.0", + "p-limit": "^3.0.0", + "pify": "^5.0.0", + "protobufjs": "~6.10.0", + "source-map": "^0.7.3", + "split": "^1.0.1" + }, + "dependencies": { + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/node": { + "version": "13.13.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.33.tgz", + "integrity": "sha512-1B3GM1yuYsFyEvBb+ljBqWBOylsWDYioZ5wpu8AhXdIhq20neXS7eaSC8GkwHE0yQYGiOIV43lMsgRYTgKZefQ==" + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "needle": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", + "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.16.0.tgz", + "integrity": "sha512-4efGA+X/YXAHLi1hN8KaPrILULaUn2nWecFrn1k2I+99HpoyvcOGEbtcOxpDiUwPF2ZANMJDh32qwOUPenuR1g==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, + "protobufjs": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", + "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -5694,14 +5552,6 @@ } } }, - "pretty-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz", - "integrity": "sha512-qG66ahoLCwpLXD09ZPHSCbUWYTqdosB7SMP4OffgTgL2PBKXMuUsrk5Bwg8q4qPkjTXsKBMr+YK3Ltd/6F9s/Q==", - "requires": { - "parse-ms": "^2.0.0" - } - }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -6019,31 +5869,6 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, - "require-in-the-middle": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.1.tgz", - "integrity": "sha512-EfkM2zANyGkrfIExsECMeNn/uzjvHrE9h36yLXSavmrDiH4tgDNvltAmEKnt4PNLbqKPHZz+uszW2wTKrLUX0w==", - "requires": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.12.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "require-like": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", @@ -6094,11 +5919,6 @@ "any-promise": "^1.3.0" } }, - "retry-axios": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz", - "integrity": "sha512-jp4YlI0qyDFfXiXGhkCOliBN1G7fRH03Nqy8YdShzGqbY5/9S2x/IR6C88ls2DFkbWuL3ASkP7QD3pVrNpPgwQ==" - }, "retry-request": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz", @@ -6398,6 +6218,15 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -6504,11 +6333,6 @@ "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", "dev": true }, - "statsd-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", - "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" - }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -6934,6 +6758,11 @@ "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", "dev": true }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -7372,6 +7201,11 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" } } } diff --git a/package.json b/package.json index 8c1e2f84..f91ddf0a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "author": "James Allen <james@sharelatex.com>", "dependencies": { + "@overleaf/metrics": "^3.4.1", "async": "3.2.0", "body-parser": "^1.19.0", "diskusage": "^1.1.3", @@ -29,8 +30,6 @@ "lockfile": "^1.0.4", "lodash": "^4.17.20", "logger-sharelatex": "^2.2.0", - "lynx": "0.2.0", - "metrics-sharelatex": "^2.6.0", "mysql": "^2.18.1", "request": "^2.88.2", "sequelize": "^5.21.5", From 5ddb95b9a215ca9a00e1e9f8ba78a53217a60293 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 25 Nov 2020 13:33:54 +0000 Subject: [PATCH 590/709] [misc] work around missing stubs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f91ddf0a..b6f4156b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit:_run": "mocha --exit --recursive --reporter spec $@ test/unit/js", "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", "lint": "node_modules/.bin/eslint --max-warnings 0 .", From 50d187c2f5d2c4d6f8f1c67db607c657e53a47c5 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 2 Dec 2020 12:25:26 +0000 Subject: [PATCH 591/709] [misc] install settings-sharelatex from npm --- package-lock.json | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23e4179f..e3f93b08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6106,8 +6106,9 @@ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, "settings-sharelatex": { - "version": "git+https://github.com/sharelatex/settings-sharelatex.git#93f63d029b52fef8825c3a401b2b6a7ba29b4750", - "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/settings-sharelatex/-/settings-sharelatex-1.1.0.tgz", + "integrity": "sha512-f7D+0lnlohoteSn6IKTH72NE+JnAdMWTKwQglAuimZWTID2FRRItZSGeYMTRpvEnaQApkoVwRp//WRMsiddnqw==", "requires": { "coffee-script": "1.6.0" } diff --git a/package.json b/package.json index b6f4156b..8d74faac 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "mysql": "^2.18.1", "request": "^2.88.2", "sequelize": "^5.21.5", - "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.1.0", + "settings-sharelatex": "^1.1.0", "sqlite3": "^4.1.1", "v8-profiler-node8": "^6.1.1", "wrench": "~1.5.9" From b73a12d12b850c4fdb24fe02fe196ad3b901580f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Dec 2020 11:02:53 +0000 Subject: [PATCH 592/709] Bump ini from 1.3.5 to 1.3.8 Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8) Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3f93b08..145ec532 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3417,9 +3417,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "inquirer": { "version": "7.1.0", From a5421f823d2993b289b7184707d8a5fa308acf02 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 17 Dec 2020 11:49:02 +0000 Subject: [PATCH 593/709] remove Array.from --- app/js/ResourceStateManager.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 07cc7857..08a31683 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -6,7 +6,6 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments @@ -56,12 +55,10 @@ module.exports = ResourceStateManager = { }) } else { logger.log({ state, basePath }, 'writing sync state') - const resourceList = Array.from(resources).map( - (resource) => resource.path - ) + const resourceList = resources.map((resource) => resource.path) return fs.writeFile( stateFile, - [...Array.from(resourceList), `stateHash:${state}`].join('\n'), + [...resourceList, `stateHash:${state}`].join('\n'), callback ) } @@ -104,7 +101,7 @@ module.exports = ResourceStateManager = { new Errors.FilesOutOfSyncError('invalid state for incremental update') ) } else { - const resources = Array.from(resourceList).map((path) => ({ path })) + const resources = resourceList.map((path) => ({ path })) return callback(null, resources) } }) @@ -116,7 +113,7 @@ module.exports = ResourceStateManager = { if (callback == null) { callback = function (error) {} } - for (file of Array.from(resources || [])) { + for (file of resources || []) { for (const dir of Array.from( __guard__(file != null ? file.path : undefined, (x) => x.split('/')) )) { @@ -127,10 +124,10 @@ module.exports = ResourceStateManager = { } // check if any of the input files are not present in list of files const seenFile = {} - for (file of Array.from(allFiles)) { + for (file of allFiles) { seenFile[file] = true } - const missingFiles = Array.from(resources) + const missingFiles = resources .filter((resource) => !seenFile[resource.path]) .map((resource) => resource.path) if ((missingFiles != null ? missingFiles.length : undefined) > 0) { @@ -146,7 +143,7 @@ module.exports = ResourceStateManager = { } else { return callback() } - } + }, } function __guard__(value, transform) { From 56041208d72984f235c982badf75c9ae67b663fd Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 17 Dec 2020 11:59:15 +0000 Subject: [PATCH 594/709] remove guard function --- app/js/ResourceStateManager.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 08a31683..ef7fd430 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -7,7 +7,6 @@ /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns - * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md @@ -84,10 +83,7 @@ module.exports = ResourceStateManager = { 'project state file truncated' ) } - const array = - __guard__(result != null ? result.toString() : undefined, (x) => - x.split('\n') - ) || [] + const array = result.toString().split('\n') const adjustedLength = Math.max(array.length, 1) const resourceList = array.slice(0, adjustedLength - 1) const oldState = array[adjustedLength - 1] @@ -114,10 +110,9 @@ module.exports = ResourceStateManager = { callback = function (error) {} } for (file of resources || []) { - for (const dir of Array.from( - __guard__(file != null ? file.path : undefined, (x) => x.split('/')) - )) { - if (dir === '..') { + if (file && file.path) { + const dirs = file.path.split('/') + if (dirs.indexOf('..') !== -1) { return callback(new Error('relative path in resource file list')) } } @@ -145,9 +140,3 @@ module.exports = ResourceStateManager = { } }, } - -function __guard__(value, transform) { - return typeof value !== 'undefined' && value !== null - ? transform(value) - : undefined -} From 373e3cd7966c4818b5483f794c3e8d3f5d3f81ac Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 17 Dec 2020 12:01:14 +0000 Subject: [PATCH 595/709] remove unnecessary returns --- app/js/ResourceStateManager.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index ef7fd430..c2b65be4 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -6,7 +6,6 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns * DS201: Simplify complex destructure assignments * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md @@ -45,7 +44,7 @@ module.exports = ResourceStateManager = { if (state == null) { // remove the file if no state passed in logger.log({ state, basePath }, 'clearing sync state') - return fs.unlink(stateFile, function (err) { + fs.unlink(stateFile, function (err) { if (err != null && err.code !== 'ENOENT') { return callback(err) } else { @@ -55,7 +54,7 @@ module.exports = ResourceStateManager = { } else { logger.log({ state, basePath }, 'writing sync state') const resourceList = resources.map((resource) => resource.path) - return fs.writeFile( + fs.writeFile( stateFile, [...resourceList, `stateHash:${state}`].join('\n'), callback @@ -69,7 +68,7 @@ module.exports = ResourceStateManager = { } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) const size = this.SYNC_STATE_MAX_SIZE - return SafeReader.readFile(stateFile, size, 'utf8', function ( + SafeReader.readFile(stateFile, size, 'utf8', function ( err, result, bytesRead @@ -98,7 +97,7 @@ module.exports = ResourceStateManager = { ) } else { const resources = resourceList.map((path) => ({ path })) - return callback(null, resources) + callback(null, resources) } }) }, @@ -136,7 +135,7 @@ module.exports = ResourceStateManager = { ) ) } else { - return callback() + callback() } }, } From dff637e9a40d35d11b2e55271e704e0b39a81870 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 17 Dec 2020 12:05:17 +0000 Subject: [PATCH 596/709] remove unnecessary null checks --- app/js/ResourceStateManager.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index c2b65be4..42e06d3a 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -7,7 +7,6 @@ /* * decaffeinate suggestions: * DS201: Simplify complex destructure assignments - * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let ResourceStateManager @@ -37,15 +36,12 @@ module.exports = ResourceStateManager = { SYNC_STATE_MAX_SIZE: 128 * 1024, saveProjectState(state, resources, basePath, callback) { - if (callback == null) { - callback = function (error) {} - } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) if (state == null) { // remove the file if no state passed in logger.log({ state, basePath }, 'clearing sync state') fs.unlink(stateFile, function (err) { - if (err != null && err.code !== 'ENOENT') { + if (err && err.code !== 'ENOENT') { return callback(err) } else { return callback() @@ -63,9 +59,6 @@ module.exports = ResourceStateManager = { }, checkProjectStateMatches(state, basePath, callback) { - if (callback == null) { - callback = function (error, resources) {} - } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) const size = this.SYNC_STATE_MAX_SIZE SafeReader.readFile(stateFile, size, 'utf8', function ( @@ -73,7 +66,7 @@ module.exports = ResourceStateManager = { result, bytesRead ) { - if (err != null) { + if (err) { return callback(err) } if (bytesRead === size) { @@ -105,9 +98,6 @@ module.exports = ResourceStateManager = { checkResourceFiles(resources, allFiles, basePath, callback) { // check the paths are all relative to current directory let file - if (callback == null) { - callback = function (error) {} - } for (file of resources || []) { if (file && file.path) { const dirs = file.path.split('/') @@ -124,7 +114,7 @@ module.exports = ResourceStateManager = { const missingFiles = resources .filter((resource) => !seenFile[resource.path]) .map((resource) => resource.path) - if ((missingFiles != null ? missingFiles.length : undefined) > 0) { + if (missingFiles && missingFiles.length > 0) { logger.err( { missingFiles, basePath, allFiles, resources }, 'missing input files for project' From b647372652eae1f78f0c8f82769700a174afec16 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 14:56:53 +0000 Subject: [PATCH 597/709] use Set instead of object --- app/js/OutputFileFinder.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index f863e0c1..35b8c5be 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -26,10 +26,9 @@ module.exports = OutputFileFinder = { if (callback == null) { callback = function (error, outputFiles, allFiles) {} } - const incomingResources = {} - for (const resource of Array.from(resources)) { - incomingResources[resource.path] = true - } + const incomingResources = new Set( + resources.map((resource) => resource.path) + ) return OutputFileFinder._getAllFiles(directory, function (error, allFiles) { if (allFiles == null) { @@ -41,10 +40,10 @@ module.exports = OutputFileFinder = { } const outputFiles = [] for (const file of Array.from(allFiles)) { - if (!incomingResources[file]) { + if (!incomingResources.has(file)) { outputFiles.push({ path: file, - type: __guard__(file.match(/\.([^\.]+)$/), (x) => x[1]) + type: __guard__(file.match(/\.([^\.]+)$/), (x) => x[1]), }) } } @@ -70,7 +69,7 @@ module.exports = OutputFileFinder = { '.archive', '-o', '-name', - '.project-*' + '.project-*', ] const args = [ directory, @@ -81,7 +80,7 @@ module.exports = OutputFileFinder = { '-o', '-type', 'f', - '-print' + '-print', ] logger.log({ args }, 'running find command') @@ -105,7 +104,7 @@ module.exports = OutputFileFinder = { }) return callback(null, fileList) }) - } + }, } function __guard__(value, transform) { From 2ad43f1e50f5ed0cde727ab3596a643703927ead Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 14:59:48 +0000 Subject: [PATCH 598/709] remove unnecessary callback code --- app/js/OutputFileFinder.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 35b8c5be..736dfedb 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -23,9 +23,6 @@ const logger = require('logger-sharelatex') module.exports = OutputFileFinder = { findOutputFiles(resources, directory, callback) { - if (callback == null) { - callback = function (error, outputFiles, allFiles) {} - } const incomingResources = new Set( resources.map((resource) => resource.path) ) @@ -52,14 +49,6 @@ module.exports = OutputFileFinder = { }, _getAllFiles(directory, _callback) { - if (_callback == null) { - _callback = function (error, fileList) {} - } - const callback = function (error, fileList) { - _callback(error, fileList) - return (_callback = function () {}) - } - // don't include clsi-specific files/directories in the output list const EXCLUDE_DIRS = [ '-name', From 305abb4e6c64cc835ecfa9df2534bbd068209611 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:01:25 +0000 Subject: [PATCH 599/709] simplify null check --- app/js/OutputFileFinder.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 736dfedb..013c6f35 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -11,7 +11,6 @@ * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ - * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let OutputFileFinder @@ -31,7 +30,7 @@ module.exports = OutputFileFinder = { if (allFiles == null) { allFiles = [] } - if (error != null) { + if (error) { logger.err({ err: error }, 'error finding all output files') return callback(error) } From 30db34355cbd3a21a885905d0e5c292ef8c7e631 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:02:40 +0000 Subject: [PATCH 600/709] remove unnecessary returns --- app/js/OutputFileFinder.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 013c6f35..20d1bf07 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -9,7 +9,6 @@ /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ @@ -43,7 +42,7 @@ module.exports = OutputFileFinder = { }) } } - return callback(null, outputFiles, allFiles) + callback(null, outputFiles, allFiles) }) }, @@ -90,7 +89,7 @@ module.exports = OutputFileFinder = { let path return (path = Path.relative(directory, file)) }) - return callback(null, fileList) + callback(null, fileList) }) }, } From 7e84cf642e9b7ad90df1049ed0a3820ace08c7d8 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:09:48 +0000 Subject: [PATCH 601/709] remove guard helper --- app/js/OutputFileFinder.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 20d1bf07..70191a26 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -36,9 +36,10 @@ module.exports = OutputFileFinder = { const outputFiles = [] for (const file of Array.from(allFiles)) { if (!incomingResources.has(file)) { + const type = Path.extname(file) outputFiles.push({ path: file, - type: __guard__(file.match(/\.([^\.]+)$/), (x) => x[1]), + type: Path.extname(file) || undefined, }) } } From c650a92bff9898bb9f666d6bf5970f3c5a0c64fa Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:10:53 +0000 Subject: [PATCH 602/709] remove Array.from --- app/js/OutputFileFinder.js | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 70191a26..e62284fe 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -1,17 +1,3 @@ -/* eslint-disable - handle-callback-err, - no-return-assign, - no-unused-vars, - no-useless-escape, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS103: Rewrite code to no longer use __guard__ - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ let OutputFileFinder const async = require('async') const fs = require('fs') @@ -34,12 +20,11 @@ module.exports = OutputFileFinder = { return callback(error) } const outputFiles = [] - for (const file of Array.from(allFiles)) { + for (const file of allFiles) { if (!incomingResources.has(file)) { - const type = Path.extname(file) outputFiles.push({ path: file, - type: Path.extname(file) || undefined, + type: Path.extname(file).replace(/^\./, '') || undefined, }) } } @@ -62,7 +47,7 @@ module.exports = OutputFileFinder = { const args = [ directory, '(', - ...Array.from(EXCLUDE_DIRS), + ...EXCLUDE_DIRS, ')', '-prune', '-o', @@ -94,9 +79,3 @@ module.exports = OutputFileFinder = { }) }, } - -function __guard__(value, transform) { - return typeof value !== 'undefined' && value !== null - ? transform(value) - : undefined -} From 8d0e79b200efdf83c78bbb95a09997f096fc719e Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:11:29 +0000 Subject: [PATCH 603/709] lint fix --- app/js/OutputFileFinder.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index e62284fe..933222e0 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -24,7 +24,7 @@ module.exports = OutputFileFinder = { if (!incomingResources.has(file)) { outputFiles.push({ path: file, - type: Path.extname(file).replace(/^\./, '') || undefined, + type: Path.extname(file).replace(/^\./, '') || undefined }) } } @@ -42,7 +42,7 @@ module.exports = OutputFileFinder = { '.archive', '-o', '-name', - '.project-*', + '.project-*' ] const args = [ directory, @@ -53,7 +53,7 @@ module.exports = OutputFileFinder = { '-o', '-type', 'f', - '-print', + '-print' ] logger.log({ args }, 'running find command') @@ -77,5 +77,5 @@ module.exports = OutputFileFinder = { }) callback(null, fileList) }) - }, + } } From 9b9fbb4159b7d948eaac73e1c98cbd159c2b4dc8 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:16:07 +0000 Subject: [PATCH 604/709] remove unnecessary return --- app/js/OutputFileFinder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 933222e0..1f62a756 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -11,7 +11,7 @@ module.exports = OutputFileFinder = { resources.map((resource) => resource.path) ) - return OutputFileFinder._getAllFiles(directory, function (error, allFiles) { + OutputFileFinder._getAllFiles(directory, function (error, allFiles) { if (allFiles == null) { allFiles = [] } @@ -61,7 +61,7 @@ module.exports = OutputFileFinder = { let stdout = '' proc.stdout.setEncoding('utf8').on('data', (chunk) => (stdout += chunk)) proc.on('error', callback) - return proc.on('close', function (code) { + proc.on('close', function (code) { if (code !== 0) { logger.warn( { directory, code }, From 1dec100dadfd8ada509e013b4ae30f0d6164f740 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:16:46 +0000 Subject: [PATCH 605/709] use once for callback --- app/js/OutputFileFinder.js | 4 +++- test/unit/js/OutputFileFinderTests.js | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 1f62a756..38fdc794 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -2,6 +2,7 @@ let OutputFileFinder const async = require('async') const fs = require('fs') const Path = require('path') +const _ = require('lodash') const { spawn } = require('child_process') const logger = require('logger-sharelatex') @@ -32,7 +33,8 @@ module.exports = OutputFileFinder = { }) }, - _getAllFiles(directory, _callback) { + _getAllFiles(directory, callback) { + callback = _.once(callback) // don't include clsi-specific files/directories in the output list const EXCLUDE_DIRS = [ '-name', diff --git a/test/unit/js/OutputFileFinderTests.js b/test/unit/js/OutputFileFinderTests.js index 4afa25e8..8744c6e7 100644 --- a/test/unit/js/OutputFileFinderTests.js +++ b/test/unit/js/OutputFileFinderTests.js @@ -28,6 +28,9 @@ describe('OutputFileFinder', function () { fs: (this.fs = {}), child_process: { spawn: (this.spawn = sinon.stub()) }, 'logger-sharelatex': { log: sinon.stub(), warn: sinon.stub() } + }, + globals: { + Math // used by lodash } }) this.directory = '/test/dir' From db515cdadd8561ab8eb9db3e0704da0cba6a8ada Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:17:20 +0000 Subject: [PATCH 606/709] clean up unnecessary var --- app/js/OutputFileFinder.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index 38fdc794..cd943047 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -74,8 +74,7 @@ module.exports = OutputFileFinder = { let fileList = stdout.trim().split('\n') fileList = fileList.map(function (file) { // Strip leading directory - let path - return (path = Path.relative(directory, file)) + return Path.relative(directory, file) }) callback(null, fileList) }) From 4ef29f04589d193bfcc3a176d4495be73eb50611 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:17:48 +0000 Subject: [PATCH 607/709] remove unnecessary requires --- app/js/OutputFileFinder.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index cd943047..d9d24996 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -1,6 +1,4 @@ let OutputFileFinder -const async = require('async') -const fs = require('fs') const Path = require('path') const _ = require('lodash') const { spawn } = require('child_process') From 8e2ca63a753442e3542b7b618fde068ab6996342 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 14:51:46 +0000 Subject: [PATCH 608/709] clean up relative path checking --- app/js/ResourceStateManager.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 42e06d3a..cd7f164a 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -98,13 +98,12 @@ module.exports = ResourceStateManager = { checkResourceFiles(resources, allFiles, basePath, callback) { // check the paths are all relative to current directory let file - for (file of resources || []) { - if (file && file.path) { - const dirs = file.path.split('/') - if (dirs.indexOf('..') !== -1) { - return callback(new Error('relative path in resource file list')) - } - } + const containsRelativePath = (resource) => { + const dirs = resource.path.split('/') + return dirs.indexOf('..') !== -1 + } + if (resources.some(containsRelativePath)) { + return callback(new Error('relative path in resource file list')) } // check if any of the input files are not present in list of files const seenFile = {} From 930f731d552b92b5f483ce0ab894603aebe30601 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 14:52:04 +0000 Subject: [PATCH 609/709] use a Set instead of an Object --- app/js/ResourceStateManager.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index cd7f164a..25083023 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -106,14 +106,11 @@ module.exports = ResourceStateManager = { return callback(new Error('relative path in resource file list')) } // check if any of the input files are not present in list of files - const seenFile = {} - for (file of allFiles) { - seenFile[file] = true - } + const seenFiles = new Set(allFiles) const missingFiles = resources - .filter((resource) => !seenFile[resource.path]) .map((resource) => resource.path) - if (missingFiles && missingFiles.length > 0) { + .filter((path) => !seenFiles.has(path)) + if (missingFiles.length > 0) { logger.err( { missingFiles, basePath, allFiles, resources }, 'missing input files for project' From bd7a7f558a79b11195a07c03797a6c21f9864368 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:44:25 +0000 Subject: [PATCH 610/709] remove unused require --- app/js/ResourceStateManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 25083023..76903f17 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -13,7 +13,6 @@ let ResourceStateManager const Path = require('path') const fs = require('fs') const logger = require('logger-sharelatex') -const settings = require('settings-sharelatex') const Errors = require('./Errors') const SafeReader = require('./SafeReader') From 78258e9558fa4e6b24001ff284bba511ffb5e721 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:45:04 +0000 Subject: [PATCH 611/709] remove unused var --- app/js/ResourceStateManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 76903f17..3407f049 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -96,7 +96,6 @@ module.exports = ResourceStateManager = { checkResourceFiles(resources, allFiles, basePath, callback) { // check the paths are all relative to current directory - let file const containsRelativePath = (resource) => { const dirs = resource.path.split('/') return dirs.indexOf('..') !== -1 From 02684ea98005101023332dde33a5cba42c84df62 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:45:20 +0000 Subject: [PATCH 612/709] remove comments --- app/js/ResourceStateManager.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 3407f049..983672ff 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -1,14 +1,3 @@ -/* eslint-disable - handle-callback-err, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS201: Simplify complex destructure assignments - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ let ResourceStateManager const Path = require('path') const fs = require('fs') From 362ac30cfa470553a9e890d32dc20401c9e559cb Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:46:16 +0000 Subject: [PATCH 613/709] remove unused module var --- app/js/ResourceStateManager.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 983672ff..7bd9df9d 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -1,11 +1,10 @@ -let ResourceStateManager const Path = require('path') const fs = require('fs') const logger = require('logger-sharelatex') const Errors = require('./Errors') const SafeReader = require('./SafeReader') -module.exports = ResourceStateManager = { +module.exports = { // The sync state is an identifier which must match for an // incremental update to be allowed. // From 5c8a51de406a93b0ebbb768825d7a0b64e241ec1 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 18 Dec 2020 15:46:36 +0000 Subject: [PATCH 614/709] format fix --- app/js/ResourceStateManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 7bd9df9d..c77c254b 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -109,5 +109,5 @@ module.exports = { } else { callback() } - }, + } } From 2747f0d40af8729304cbf716efc41a195f414e03 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 28 Dec 2020 13:16:31 +0000 Subject: [PATCH 615/709] [misc] CompileController: exact match for output.pdf The previous regex could mistake user provided pdf files, like `fake_output.pdf`, as the final output file. The frontend expects to find a `output.pdf` file on success. --- app/js/CompileController.js | 6 +--- test/unit/js/CompileControllerTests.js | 41 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/app/js/CompileController.js b/app/js/CompileController.js index fb7367ce..b0ce3bb6 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -80,11 +80,7 @@ module.exports = CompileController = { let file status = 'failure' for (file of Array.from(outputFiles)) { - if ( - file.path != null - ? file.path.match(/output\.pdf$/) - : undefined - ) { + if (file.path === 'output.pdf') { status = 'success' } } diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 1c93c96e..70f67b60 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -129,6 +129,47 @@ describe('CompileController', function () { }) }) + describe('with user provided fake_output.pdf', function () { + beforeEach(function () { + this.output_files = [ + { + path: 'fake_output.pdf', + type: 'pdf', + build: 1234 + }, + { + path: 'output.log', + type: 'log', + build: 1234 + } + ] + this.CompileManager.doCompileWithLock = sinon + .stub() + .callsArgWith(1, null, this.output_files) + this.CompileController.compile(this.req, this.res) + }) + + it('should return the JSON response with status failure', function () { + this.res.status.calledWith(200).should.equal(true) + this.res.send + .calledWith({ + compile: { + status: 'failure', + error: null, + outputFiles: this.output_files.map((file) => { + return { + url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, + path: file.path, + type: file.type, + build: file.build + } + }) + } + }) + .should.equal(true) + }) + }) + describe('with an error', function () { beforeEach(function () { this.CompileManager.doCompileWithLock = sinon From bd7920d4c1bd9fe42326251e08111800ec36f355 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 5 Jan 2021 18:26:53 +0000 Subject: [PATCH 616/709] [misc] bump the node version to 10.23.1 --- .nvmrc | 2 +- Dockerfile | 2 +- buildscript.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nvmrc b/.nvmrc index c2f64213..2baa2d43 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.22.1 +10.23.1 diff --git a/Dockerfile b/Dockerfile index 5d702dce..4be4353c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -FROM node:10.22.1 as base +FROM node:10.23.1 as base WORKDIR /app COPY install_deps.sh /app diff --git a/buildscript.txt b/buildscript.txt index 6794c8cb..b2093150 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -4,6 +4,6 @@ clsi --docker-repos=gcr.io/overleaf-ops --env-add= --env-pass-through=TEXLIVE_IMAGE ---node-version=10.22.1 +--node-version=10.23.1 --public-repo=True --script-version=3.4.0 From faa443c7def7250e270b36fc6ea8b58d32851489 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 15 Dec 2020 12:03:25 +0000 Subject: [PATCH 617/709] rename staticServer to staticCompileServer --- app.js | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/app.js b/app.js index 9402869c..05bcffce 100644 --- a/app.js +++ b/app.js @@ -118,21 +118,25 @@ const ForbidSymlinks = require('./app/js/StaticServerForbidSymlinks') // create a static server which does not allow access to any symlinks // avoids possible mismatch of root directory between middleware check // and serving the files -const staticServer = ForbidSymlinks(express.static, Settings.path.compilesDir, { - setHeaders(res, path, stat) { - if (Path.basename(path) === 'output.pdf') { - // Calculate an etag in the same way as nginx - // https://github.com/tj/send/issues/65 - const etag = (path, stat) => - `"${Math.ceil(+stat.mtime / 1000).toString(16)}` + - '-' + - Number(stat.size).toString(16) + - '"' - res.set('Etag', etag(path, stat)) +const staticCompileServer = ForbidSymlinks( + express.static, + Settings.path.compilesDir, + { + setHeaders(res, path, stat) { + if (Path.basename(path) === 'output.pdf') { + // Calculate an etag in the same way as nginx + // https://github.com/tj/send/issues/65 + const etag = (path, stat) => + `"${Math.ceil(+stat.mtime / 1000).toString(16)}` + + '-' + + Number(stat.size).toString(16) + + '"' + res.set('Etag', etag(path, stat)) + } + return res.set('Content-Type', ContentTypeMapper.map(path)) } - return res.set('Content-Type', ContentTypeMapper.map(path)) } -}) +) app.get( '/project/:project_id/user/:user_id/build/:build_id/output/*', @@ -141,7 +145,7 @@ app.get( req.url = `/${req.params.project_id}-${req.params.user_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) - return staticServer(req, res, next) + return staticCompileServer(req, res, next) } ) @@ -154,7 +158,7 @@ app.get('/project/:project_id/build/:build_id/output/*', function ( req.url = `/${req.params.project_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) - return staticServer(req, res, next) + return staticCompileServer(req, res, next) }) app.get('/project/:project_id/user/:user_id/output/*', function ( @@ -164,7 +168,7 @@ app.get('/project/:project_id/user/:user_id/output/*', function ( ) { // for specific user get the path to the top level file req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}` - return staticServer(req, res, next) + return staticCompileServer(req, res, next) }) app.get('/project/:project_id/output/*', function (req, res, next) { @@ -179,7 +183,7 @@ app.get('/project/:project_id/output/*', function (req, res, next) { } else { req.url = `/${req.params.project_id}/${req.params[0]}` } - return staticServer(req, res, next) + return staticCompileServer(req, res, next) }) app.get('/oops', function (req, res, next) { From 6e06af3e9eb9788114b20230761c25e7befb869c Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 15 Dec 2020 14:59:05 +0000 Subject: [PATCH 618/709] add output directory --- Dockerfile | 4 ++-- app.js | 24 ++++++++++++++++++++++-- app/js/CompileManager.js | 6 ++++++ app/js/OutputCacheManager.js | 24 ++++++++++++++++-------- config/settings.defaults.js | 1 + 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4be4353c..7eaa03f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ COPY . /app FROM base COPY --from=app /app /app -RUN mkdir -p cache compiles db \ -&& chown node:node cache compiles db +RUN mkdir -p cache compiles db output \ + && chown node:node cache compiles db output CMD ["node", "--expose-gc", "app.js"] diff --git a/app.js b/app.js index 05bcffce..c87edeb3 100644 --- a/app.js +++ b/app.js @@ -138,6 +138,26 @@ const staticCompileServer = ForbidSymlinks( } ) +const staticOutputServer = ForbidSymlinks( + express.static, + Settings.path.outputDir, + { + setHeaders(res, path, stat) { + if (Path.basename(path) === 'output.pdf') { + // Calculate an etag in the same way as nginx + // https://github.com/tj/send/issues/65 + const etag = (path, stat) => + `"${Math.ceil(+stat.mtime / 1000).toString(16)}` + + '-' + + Number(stat.size).toString(16) + + '"' + res.set('Etag', etag(path, stat)) + } + return res.set('Content-Type', ContentTypeMapper.map(path)) + } + } +) + app.get( '/project/:project_id/user/:user_id/build/:build_id/output/*', function (req, res, next) { @@ -145,7 +165,7 @@ app.get( req.url = `/${req.params.project_id}-${req.params.user_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) - return staticCompileServer(req, res, next) + return staticOutputServer(req, res, next) } ) @@ -158,7 +178,7 @@ app.get('/project/:project_id/build/:build_id/output/*', function ( req.url = `/${req.params.project_id}/` + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) - return staticCompileServer(req, res, next) + return staticOutputServer(req, res, next) }) app.get('/project/:project_id/user/:user_id/output/*', function ( diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 68edde49..5dab554f 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -46,6 +46,9 @@ const getCompileName = function (project_id, user_id) { const getCompileDir = (project_id, user_id) => Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id)) +const getOutputDir = (project_id, user_id) => + Path.join(Settings.path.outputDir, getCompileName(project_id, user_id)) + module.exports = CompileManager = { doCompileWithLock(request, callback) { if (callback == null) { @@ -72,6 +75,8 @@ module.exports = CompileManager = { callback = function (error, outputFiles) {} } const compileDir = getCompileDir(request.project_id, request.user_id) + const outputDir = getOutputDir(request.project_id, request.user_id) + let timer = new Metrics.Timer('write-to-disk') logger.log( { project_id: request.project_id, user_id: request.user_id }, @@ -294,6 +299,7 @@ module.exports = CompileManager = { return OutputCacheManager.saveOutputFiles( outputFiles, compileDir, + outputDir, (error, newOutputFiles) => callback(null, newOutputFiles) ) } diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index b34dea79..8aefb9b7 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -26,8 +26,8 @@ const crypto = require('crypto') const OutputFileOptimiser = require('./OutputFileOptimiser') module.exports = OutputCacheManager = { - CACHE_SUBDIR: '.cache/clsi', - ARCHIVE_SUBDIR: '.archive/clsi', + CACHE_SUBDIR: 'generated-files', + ARCHIVE_SUBDIR: 'archived-logs', // build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes // for backwards compatibility, make the randombytes part optional BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/, @@ -59,7 +59,7 @@ module.exports = OutputCacheManager = { }) }, - saveOutputFiles(outputFiles, compileDir, callback) { + saveOutputFiles(outputFiles, compileDir, outputDir, callback) { if (callback == null) { callback = function (error) {} } @@ -70,22 +70,29 @@ module.exports = OutputCacheManager = { return OutputCacheManager.saveOutputFilesInBuildDir( outputFiles, compileDir, + outputDir, buildId, callback ) }) }, - saveOutputFilesInBuildDir(outputFiles, compileDir, buildId, callback) { + saveOutputFilesInBuildDir( + outputFiles, + compileDir, + outputDir, + buildId, + callback + ) { // make a compileDir/CACHE_SUBDIR/build_id directory and // copy all the output files into it if (callback == null) { callback = function (error) {} } - const cacheRoot = Path.join(compileDir, OutputCacheManager.CACHE_SUBDIR) + const cacheRoot = Path.join(outputDir, OutputCacheManager.CACHE_SUBDIR) // Put the files into a new cache subdirectory const cacheDir = Path.join( - compileDir, + outputDir, OutputCacheManager.CACHE_SUBDIR, buildId ) @@ -102,6 +109,7 @@ module.exports = OutputCacheManager = { OutputCacheManager.archiveLogs( outputFiles, compileDir, + outputDir, buildId, function (err) { if (err != null) { @@ -198,12 +206,12 @@ module.exports = OutputCacheManager = { }) }, - archiveLogs(outputFiles, compileDir, buildId, callback) { + archiveLogs(outputFiles, compileDir, outputDir, buildId, callback) { if (callback == null) { callback = function (error) {} } const archiveDir = Path.join( - compileDir, + outputDir, OutputCacheManager.ARCHIVE_SUBDIR, buildId ) diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 823f1f73..72c3471b 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -27,6 +27,7 @@ module.exports = { path: { compilesDir: Path.resolve(__dirname, '../compiles'), + outputDir: Path.resolve(__dirname, '../output'), clsiCacheDir: Path.resolve(__dirname, '../cache'), synctexBaseDir(projectId) { return Path.join(this.compilesDir, projectId) From c9a9b7a2165d55e246371edd842bfb83e98f4bbc Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 16 Dec 2020 11:18:18 +0000 Subject: [PATCH 619/709] add git ignore for output directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b32ea209..dee3eca1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules test/acceptance/fixtures/tmp compiles +output .DS_Store *~ cache From 7b1550a4526203aaf473d1e1f9e69bb0226f9746 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 16 Dec 2020 15:14:18 +0000 Subject: [PATCH 620/709] clear output directory when clearing project --- app/js/CompileManager.js | 13 +++++++++++-- test/unit/js/CompileManagerTests.js | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 5dab554f..25741005 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -329,6 +329,7 @@ module.exports = CompileManager = { } const compileDir = getCompileDir(project_id, user_id) + const outputDir = getOutputDir(project_id, user_id) return CompileManager._checkDirectory(compileDir, function (err, exists) { if (err != null) { @@ -338,7 +339,13 @@ module.exports = CompileManager = { return callback() } // skip removal if no directory present - const proc = child_process.spawn('rm', ['-r', compileDir]) + const proc = child_process.spawn('rm', [ + '-r', + '-f', + '--', + compileDir, + outputDir + ]) proc.on('error', callback) @@ -349,7 +356,9 @@ module.exports = CompileManager = { if (code === 0) { return callback(null) } else { - return callback(new Error(`rm -r ${compileDir} failed: ${stderr}`)) + return callback( + new Error(`rm -r ${compileDir} ${outputDir} failed: ${stderr}`) + ) } }) }) diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index cb85f2f1..917dfe8c 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -34,7 +34,8 @@ describe('CompileManager', function () { './OutputCacheManager': (this.OutputCacheManager = {}), 'settings-sharelatex': (this.Settings = { path: { - compilesDir: '/compiles/dir' + compilesDir: '/compiles/dir', + outputDir: '/output/dir' }, synctexBaseDir() { return '/compile' @@ -166,6 +167,7 @@ describe('CompileManager', function () { this.env = {} this.Settings.compileDir = 'compiles' this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + this.outputDir = `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}` this.ResourceWriter.syncResourcesToDisk = sinon .stub() .callsArgWith(2, null, this.resources) @@ -175,7 +177,7 @@ describe('CompileManager', function () { .callsArgWith(2, null, this.output_files) this.OutputCacheManager.saveOutputFiles = sinon .stub() - .callsArgWith(2, null, this.build_files) + .callsArgWith(3, null, this.build_files) this.DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) return (this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false)) }) @@ -312,7 +314,10 @@ describe('CompileManager', function () { return this.child_process.spawn .calledWith('rm', [ '-r', - `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + '-f', + '--', + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}` ]) .should.equal(true) }) @@ -348,7 +353,10 @@ describe('CompileManager', function () { return this.child_process.spawn .calledWith('rm', [ '-r', - `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` + '-f', + '--', + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}` ]) .should.equal(true) }) @@ -357,7 +365,7 @@ describe('CompileManager', function () { this.callback.calledWithExactly(sinon.match(Error)).should.equal(true) this.callback.args[0][0].message.should.equal( - `rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} failed: ${this.error}` + `rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} ${this.Settings.path.outputDir}/${this.project_id}-${this.user_id} failed: ${this.error}` ) }) }) From 6fb99a37aa99ac5e6a777a6498ee45b8c5db1395 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 22 Jan 2021 11:03:47 +0000 Subject: [PATCH 621/709] add a warning for requests without build id --- app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.js b/app.js index c87edeb3..930ebc3e 100644 --- a/app.js +++ b/app.js @@ -187,11 +187,13 @@ app.get('/project/:project_id/user/:user_id/output/*', function ( next ) { // for specific user get the path to the top level file + logger.warn({ url: req.url }, 'direct request for file in compile directory') req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}` return staticCompileServer(req, res, next) }) app.get('/project/:project_id/output/*', function (req, res, next) { + logger.warn({ url: req.url }, 'direct request for file in compile directory') if ( (req.query != null ? req.query.build : undefined) != null && req.query.build.match(OutputCacheManager.BUILD_REGEX) From 6f2985323bf4a9588e785d213020b0b67f336c2d Mon Sep 17 00:00:00 2001 From: Brian Gough <briangough@users.noreply.github.com> Date: Mon, 25 Jan 2021 15:26:53 +0000 Subject: [PATCH 622/709] Revert "decaff cleanup ResourceStateManager" --- app/js/ResourceStateManager.js | 85 +++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index c77c254b..07cc7857 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -1,10 +1,27 @@ +/* eslint-disable + handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ResourceStateManager const Path = require('path') const fs = require('fs') const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') const Errors = require('./Errors') const SafeReader = require('./SafeReader') -module.exports = { +module.exports = ResourceStateManager = { // The sync state is an identifier which must match for an // incremental update to be allowed. // @@ -23,12 +40,15 @@ module.exports = { SYNC_STATE_MAX_SIZE: 128 * 1024, saveProjectState(state, resources, basePath, callback) { + if (callback == null) { + callback = function (error) {} + } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) if (state == null) { // remove the file if no state passed in logger.log({ state, basePath }, 'clearing sync state') - fs.unlink(stateFile, function (err) { - if (err && err.code !== 'ENOENT') { + return fs.unlink(stateFile, function (err) { + if (err != null && err.code !== 'ENOENT') { return callback(err) } else { return callback() @@ -36,24 +56,29 @@ module.exports = { }) } else { logger.log({ state, basePath }, 'writing sync state') - const resourceList = resources.map((resource) => resource.path) - fs.writeFile( + const resourceList = Array.from(resources).map( + (resource) => resource.path + ) + return fs.writeFile( stateFile, - [...resourceList, `stateHash:${state}`].join('\n'), + [...Array.from(resourceList), `stateHash:${state}`].join('\n'), callback ) } }, checkProjectStateMatches(state, basePath, callback) { + if (callback == null) { + callback = function (error, resources) {} + } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) const size = this.SYNC_STATE_MAX_SIZE - SafeReader.readFile(stateFile, size, 'utf8', function ( + return SafeReader.readFile(stateFile, size, 'utf8', function ( err, result, bytesRead ) { - if (err) { + if (err != null) { return callback(err) } if (bytesRead === size) { @@ -62,7 +87,10 @@ module.exports = { 'project state file truncated' ) } - const array = result.toString().split('\n') + const array = + __guard__(result != null ? result.toString() : undefined, (x) => + x.split('\n') + ) || [] const adjustedLength = Math.max(array.length, 1) const resourceList = array.slice(0, adjustedLength - 1) const oldState = array[adjustedLength - 1] @@ -76,27 +104,36 @@ module.exports = { new Errors.FilesOutOfSyncError('invalid state for incremental update') ) } else { - const resources = resourceList.map((path) => ({ path })) - callback(null, resources) + const resources = Array.from(resourceList).map((path) => ({ path })) + return callback(null, resources) } }) }, checkResourceFiles(resources, allFiles, basePath, callback) { // check the paths are all relative to current directory - const containsRelativePath = (resource) => { - const dirs = resource.path.split('/') - return dirs.indexOf('..') !== -1 + let file + if (callback == null) { + callback = function (error) {} } - if (resources.some(containsRelativePath)) { - return callback(new Error('relative path in resource file list')) + for (file of Array.from(resources || [])) { + for (const dir of Array.from( + __guard__(file != null ? file.path : undefined, (x) => x.split('/')) + )) { + if (dir === '..') { + return callback(new Error('relative path in resource file list')) + } + } } // check if any of the input files are not present in list of files - const seenFiles = new Set(allFiles) - const missingFiles = resources + const seenFile = {} + for (file of Array.from(allFiles)) { + seenFile[file] = true + } + const missingFiles = Array.from(resources) + .filter((resource) => !seenFile[resource.path]) .map((resource) => resource.path) - .filter((path) => !seenFiles.has(path)) - if (missingFiles.length > 0) { + if ((missingFiles != null ? missingFiles.length : undefined) > 0) { logger.err( { missingFiles, basePath, allFiles, resources }, 'missing input files for project' @@ -107,7 +144,13 @@ module.exports = { ) ) } else { - callback() + return callback() } } } + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} From dcbeec9fd70b67889b6c55624be07422a4fb62b4 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 26 Jan 2021 11:03:18 +0000 Subject: [PATCH 623/709] add unit test for non-existent state file --- test/unit/js/ResourceStateManagerTests.js | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/unit/js/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js index ca2b625f..e2bc0738 100644 --- a/test/unit/js/ResourceStateManagerTests.js +++ b/test/unit/js/ResourceStateManagerTests.js @@ -122,6 +122,32 @@ describe('ResourceStateManager', function () { }) }) + describe('when the state file is not present', function () { + beforeEach(function () { + this.SafeReader.readFile = sinon.stub().callsArg(3) + return this.ResourceStateManager.checkProjectStateMatches( + this.state, + this.basePath, + this.callback + ) + }) + + it('should read the resource file', function () { + return this.SafeReader.readFile + .calledWith(this.resourceFileName) + .should.equal(true) + }) + + it('should call the callback with an error', function () { + this.callback + .calledWith(sinon.match(Errors.FilesOutOfSyncError)) + .should.equal(true) + + const message = this.callback.args[0][0].message + expect(message).to.include('invalid state for incremental update') + }) + }) + return describe('when the state does not match', function () { beforeEach(function () { this.SafeReader.readFile = sinon From d09e9e1a787fb866e5c3961ff38fbc3d63af369d Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 26 Jan 2021 11:04:33 +0000 Subject: [PATCH 624/709] Revert "Merge pull request #205 from overleaf/revert-200-bg-decaff-cleanup" This reverts commit 76d8d3181b9464d1e1bbc713a2729ca269d9c047, reversing changes made to 31a8dc3a98d73c2707d633712f0ef7207013e78b. --- app/js/ResourceStateManager.js | 85 +++++++++------------------------- 1 file changed, 21 insertions(+), 64 deletions(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index 07cc7857..c77c254b 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -1,27 +1,10 @@ -/* eslint-disable - handle-callback-err, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS103: Rewrite code to no longer use __guard__ - * DS201: Simplify complex destructure assignments - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -let ResourceStateManager const Path = require('path') const fs = require('fs') const logger = require('logger-sharelatex') -const settings = require('settings-sharelatex') const Errors = require('./Errors') const SafeReader = require('./SafeReader') -module.exports = ResourceStateManager = { +module.exports = { // The sync state is an identifier which must match for an // incremental update to be allowed. // @@ -40,15 +23,12 @@ module.exports = ResourceStateManager = { SYNC_STATE_MAX_SIZE: 128 * 1024, saveProjectState(state, resources, basePath, callback) { - if (callback == null) { - callback = function (error) {} - } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) if (state == null) { // remove the file if no state passed in logger.log({ state, basePath }, 'clearing sync state') - return fs.unlink(stateFile, function (err) { - if (err != null && err.code !== 'ENOENT') { + fs.unlink(stateFile, function (err) { + if (err && err.code !== 'ENOENT') { return callback(err) } else { return callback() @@ -56,29 +36,24 @@ module.exports = ResourceStateManager = { }) } else { logger.log({ state, basePath }, 'writing sync state') - const resourceList = Array.from(resources).map( - (resource) => resource.path - ) - return fs.writeFile( + const resourceList = resources.map((resource) => resource.path) + fs.writeFile( stateFile, - [...Array.from(resourceList), `stateHash:${state}`].join('\n'), + [...resourceList, `stateHash:${state}`].join('\n'), callback ) } }, checkProjectStateMatches(state, basePath, callback) { - if (callback == null) { - callback = function (error, resources) {} - } const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) const size = this.SYNC_STATE_MAX_SIZE - return SafeReader.readFile(stateFile, size, 'utf8', function ( + SafeReader.readFile(stateFile, size, 'utf8', function ( err, result, bytesRead ) { - if (err != null) { + if (err) { return callback(err) } if (bytesRead === size) { @@ -87,10 +62,7 @@ module.exports = ResourceStateManager = { 'project state file truncated' ) } - const array = - __guard__(result != null ? result.toString() : undefined, (x) => - x.split('\n') - ) || [] + const array = result.toString().split('\n') const adjustedLength = Math.max(array.length, 1) const resourceList = array.slice(0, adjustedLength - 1) const oldState = array[adjustedLength - 1] @@ -104,36 +76,27 @@ module.exports = ResourceStateManager = { new Errors.FilesOutOfSyncError('invalid state for incremental update') ) } else { - const resources = Array.from(resourceList).map((path) => ({ path })) - return callback(null, resources) + const resources = resourceList.map((path) => ({ path })) + callback(null, resources) } }) }, checkResourceFiles(resources, allFiles, basePath, callback) { // check the paths are all relative to current directory - let file - if (callback == null) { - callback = function (error) {} + const containsRelativePath = (resource) => { + const dirs = resource.path.split('/') + return dirs.indexOf('..') !== -1 } - for (file of Array.from(resources || [])) { - for (const dir of Array.from( - __guard__(file != null ? file.path : undefined, (x) => x.split('/')) - )) { - if (dir === '..') { - return callback(new Error('relative path in resource file list')) - } - } + if (resources.some(containsRelativePath)) { + return callback(new Error('relative path in resource file list')) } // check if any of the input files are not present in list of files - const seenFile = {} - for (file of Array.from(allFiles)) { - seenFile[file] = true - } - const missingFiles = Array.from(resources) - .filter((resource) => !seenFile[resource.path]) + const seenFiles = new Set(allFiles) + const missingFiles = resources .map((resource) => resource.path) - if ((missingFiles != null ? missingFiles.length : undefined) > 0) { + .filter((path) => !seenFiles.has(path)) + if (missingFiles.length > 0) { logger.err( { missingFiles, basePath, allFiles, resources }, 'missing input files for project' @@ -144,13 +107,7 @@ module.exports = ResourceStateManager = { ) ) } else { - return callback() + callback() } } } - -function __guard__(value, transform) { - return typeof value !== 'undefined' && value !== null - ? transform(value) - : undefined -} From f84ba5ad4cbdee1658911a7b82f45b21181141f9 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 26 Jan 2021 13:59:28 +0000 Subject: [PATCH 625/709] include fallback for missing state file --- app/js/ResourceStateManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index c77c254b..e36f19ed 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -62,7 +62,7 @@ module.exports = { 'project state file truncated' ) } - const array = result.toString().split('\n') + const array = result ? result.toString().split('\n') : [] const adjustedLength = Math.max(array.length, 1) const resourceList = array.slice(0, adjustedLength - 1) const oldState = array[adjustedLength - 1] From fc30a5034f928a1c3574f61d78a2b38f750e6bf6 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 26 Jan 2021 14:08:29 +0000 Subject: [PATCH 626/709] add uncaughtException handler --- app.js | 9 +++++++++ config/settings.defaults.js | 2 ++ 2 files changed, 11 insertions(+) diff --git a/app.js b/app.js index 930ebc3e..280b8694 100644 --- a/app.js +++ b/app.js @@ -339,6 +339,15 @@ const loadHttpPort = Settings.internal.load_balancer_agent.local_port if (!module.parent) { // Called directly + + // handle uncaught exceptions when running in production + if (Settings.catchErrors) { + process.removeAllListeners('uncaughtException') + process.on('uncaughtException', (error) => + logger.error({ err: error }, 'uncaughtException') + ) + } + app.listen(port, host, (error) => { if (error) { logger.fatal({ error }, `Error starting CLSI on ${host}:${port}`) diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 72c3471b..a0ad8433 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -25,6 +25,8 @@ module.exports = { processLifespanLimitMs: parseInt(process.env.PROCESS_LIFE_SPAN_LIMIT_MS) || 60 * 60 * 24 * 1000 * 2, + catchErrors: process.env.CATCH_ERRORS === 'true', + path: { compilesDir: Path.resolve(__dirname, '../compiles'), outputDir: Path.resolve(__dirname, '../output'), From 68b98e54189540a5c0b8e42fca031efc0b33316f Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 26 Jan 2021 16:35:39 +0000 Subject: [PATCH 627/709] provide a /oops-internal endpoint for testing uncaughtExceptions --- app.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app.js b/app.js index 280b8694..aee8d448 100644 --- a/app.js +++ b/app.js @@ -213,6 +213,12 @@ app.get('/oops', function (req, res, next) { return res.send('error\n') }) +app.get('/oops-internal', function (req, res, next) { + setTimeout(function () { + throw new Error('Test error') + }, 1) +}) + app.get('/status', (req, res, next) => res.send('CLSI is alive\n')) Settings.processTooOld = false From ad6fde9ee62e5860ee64526c10fafe0c856ae240 Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Thu, 18 Feb 2021 15:09:48 +0000 Subject: [PATCH 628/709] Ensure that app folders exist before running chown --- entrypoint.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 26965748..14921d17 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -8,9 +8,9 @@ groupadd --non-unique --gid ${DOCKER_GROUP} dockeronhost usermod -aG dockeronhost node # compatibility: initial volume setup -chown node:node /app/cache -chown node:node /app/compiles -chown node:node /app/db +mkdir -p /app/cache && chown node:node /app/cache +mkdir -p /app/compiles && chown node:node /app/compiles +mkdir -p /app/db && chown node:node /app/db # make synctex available for remount in compiles cp /app/bin/synctex /app/bin/synctex-mount/synctex From a440064a50008dad3e03b40a74c76c05c608e085 Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Thu, 18 Feb 2021 15:33:16 +0000 Subject: [PATCH 629/709] Add /app/output --- entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/entrypoint.sh b/entrypoint.sh index 14921d17..b05a773e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -11,6 +11,7 @@ usermod -aG dockeronhost node mkdir -p /app/cache && chown node:node /app/cache mkdir -p /app/compiles && chown node:node /app/compiles mkdir -p /app/db && chown node:node /app/db +mkdir -p /app/output && chown node:node /app/output # make synctex available for remount in compiles cp /app/bin/synctex /app/bin/synctex-mount/synctex From 9fe4a42d2b7b84441b53407beefb98868f62d5fd Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Wed, 10 Mar 2021 22:36:33 +0000 Subject: [PATCH 630/709] Update README.md --- README.md | 69 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 02cd0a47..2cc946cb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A web api for compiling LaTeX documents in the cloud The Common LaTeX Service Interface (CLSI) provides a RESTful interface to traditional LaTeX tools (or, more generally, any command line tool for composing marked-up documents into a display format such as PDF or HTML). The CLSI listens on the following ports by default: -* TCP/3009 - the RESTful interface +* TCP/3013 - the RESTful interface * TCP/3048 - reports load information * TCP/3049 - HTTP interface to control the CLSI service @@ -33,25 +33,41 @@ Installation The CLSI can be installed and set up as part of the entire [Overleaf stack](https://github.com/overleaf/overleaf) (complete with front end editor and document storage), or it can be run as a standalone service. To run is as a standalone service, first checkout this repository: $ git clone git@github.com:overleaf/clsi.git - + Then install the require npm modules: $ npm install - -Then compile the coffee script source files: - $ grunt install - -Finally, (after configuring your local database - see the Config section), run the CLSI service: +Then build the Docker image: + + $ docker build . -t overleaf/clsi + +Then pull the TeXLive image: + + $ docker pull texlive/texlive + +Then start the Docker container: + + $ docker run --rm \ + -p 127.0.0.1:3013:3013 \ + -e LISTEN_ADDRESS=0.0.0.0 \ + -e DOCKER_RUNNER=true \ + -e TEXLIVE_IMAGE=texlive/texlive \ + -e TEXLIVE_IMAGE_USER=root \ + -e COMPILES_HOST_DIR="$PWD/compiles" \ + -v "$PWD/compiles:/app/compiles" \ + -v "$PWD/cache:/app/cache" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + overleaf/clsi + +Note: if you're running the CLSI in macOS you may need to use `-v /var/run/docker.sock.raw:/var/run/docker.sock` instead. + +The CLSI should then be running at <http://localhost:3013> - $ grunt run - -The CLSI should then be running at http://localhost:3013. - Config ------ -You will need to set up a database in mysql to use with the CLSI, and then fill in the database name, username and password in the config file at `config/settings.development.coffee`. +The CLSI will use a SQLite database by default, but you can optionally set up a MySQL database and then fill in the database name, username and password in the config file at `config/settings.development.coffee`. API --- @@ -64,35 +80,38 @@ The CLSI is based on a JSON API. POST /project/<project-id>/compile -```javascript +```json5 { "compile": { "options": { // Which compiler to use. Can be latex, pdflatex, xelatex or lualatex "compiler": "lualatex", // How many seconds to wait before killing the process. Default is 60. - "timeout": 40 + "timeout": 40 }, // The main file to run LaTeX on - "rootResourcePath": "main.tex", + "rootResourcePath": "main.tex", // An array of files to include in the compilation. May have either the content // passed directly, or a URL where it can be downloaded. - "resources": [{ + "resources": [ + { "path": "main.tex", "content": "\\documentclass{article}\n\\begin{document}\nHello World\n\\end{document}" - }, { - "path": "image.png", - "url": "www.example.com/image.png", - "modified": 123456789 // Unix time since epoch - }] + } + // ,{ + // "path": "image.png", + // "url": "www.example.com/image.png", + // "modified": 123456789 // Unix time since epoch + // } + ] } } ``` -With `curl`, if you place the above json in a file called `data.json`, the request would look like this: +With `curl`, if you place the above JSON in a file called `data.json`, the request would look like this: ``` shell -$ curl -X POST -d @data.json localhost:3013/project/<id>/compile +$ curl -X POST -H 'Content-Type: application/json' -d @data.json http://localhost:3013/project/<id>/compile ``` You can specify any project-id in the URL, and the files and LaTeX environment will be persisted between requests. @@ -100,7 +119,7 @@ URLs will be downloaded and cached until provided with a more recent modified da #### Example Response -```javascript +```json { "compile": { "status": "success", @@ -120,4 +139,4 @@ License The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the `LICENSE` file. -Copyright (c) Overleaf, 2014-2019. +Copyright (c) Overleaf, 2014-2021. From 4c7bd98a19fa29929fdf808115887032f3be611d Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Wed, 10 Mar 2021 22:38:34 +0000 Subject: [PATCH 631/709] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 2cc946cb..f8f4ef3a 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,6 @@ The CLSI can be installed and set up as part of the entire [Overleaf stack](http $ git clone git@github.com:overleaf/clsi.git -Then install the require npm modules: - - $ npm install - Then build the Docker image: $ docker build . -t overleaf/clsi From 4311bc9175a94c27cdff541bd26ca41ebf361fd4 Mon Sep 17 00:00:00 2001 From: Alf Eaton <75253002+aeaton-overleaf@users.noreply.github.com> Date: Thu, 11 Mar 2021 11:50:47 +0000 Subject: [PATCH 632/709] Change settings file .coffee to .js --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f8f4ef3a..c4c1eabf 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The Common LaTeX Service Interface (CLSI) provides a RESTful interface to tradit * TCP/3048 - reports load information * TCP/3049 - HTTP interface to control the CLSI service -These defaults can be modified in `config/settings.defaults.coffee`. +These defaults can be modified in `config/settings.defaults.js`. The provided `Dockerfile` builds a docker image which has the docker command line tools installed. The configuration in `docker-compose-config.yml` mounts the docker socket, in order that the CLSI container can talk to the docker host it is running in. This allows it to spin up `sibling containers` running an image with a TeX distribution installed to perform the actual compiles. @@ -63,7 +63,7 @@ The CLSI should then be running at <http://localhost:3013> Config ------ -The CLSI will use a SQLite database by default, but you can optionally set up a MySQL database and then fill in the database name, username and password in the config file at `config/settings.development.coffee`. +The CLSI will use a SQLite database by default, but you can optionally set up a MySQL database and then fill in the database name, username and password in the config file at `config/settings.development.js`. API --- From 8fbe688d373dc2b9c046492b6d40513f4234b42e Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Fri, 12 Mar 2021 16:18:36 -0500 Subject: [PATCH 633/709] Add a global test setup file Set up generally useful stuff: * chai.should() * logger stubs * globals in SandboxedModule, including Buffer and process, which are now required in Node 12 --- .mocharc.json | 3 +++ test/acceptance/js/BrokenLatexFileTests.js | 1 - test/acceptance/js/DeleteOldFilesTest.js | 1 - test/acceptance/js/ExampleDocumentTests.js | 1 - test/acceptance/js/SimpleLatexFileTests.js | 1 - test/acceptance/js/SynctexTests.js | 1 - test/acceptance/js/TimeoutTests.js | 1 - test/acceptance/js/UrlCachingTests.js | 1 - test/acceptance/js/WordcountTests.js | 1 - test/setup.js | 19 +++++++++++++++++++ test/unit/js/CompileControllerTests.js | 9 +-------- test/unit/js/CompileManagerTests.js | 2 -- test/unit/js/ContentTypeMapperTests.js | 1 - test/unit/js/DockerLockManagerTests.js | 7 +------ test/unit/js/DockerRunnerTests.js | 7 ------- test/unit/js/DraftModeManagerTests.js | 4 +--- test/unit/js/LatexRunnerTests.js | 5 ----- test/unit/js/LockManagerTests.js | 6 ------ test/unit/js/OutputFileFinderTests.js | 4 +--- test/unit/js/OutputFileOptimiserTests.js | 2 -- .../unit/js/ProjectPersistenceManagerTests.js | 6 ------ test/unit/js/RequestParserTests.js | 1 - test/unit/js/ResourceStateManagerTests.js | 2 -- test/unit/js/ResourceWriterTests.js | 4 +--- .../js/StaticServerForbidSymlinksTests.js | 6 ------ test/unit/js/TikzManager.js | 4 +--- test/unit/js/UrlCacheTests.js | 2 -- test/unit/js/UrlFetcherTests.js | 5 ----- 28 files changed, 28 insertions(+), 79 deletions(-) create mode 100644 .mocharc.json create mode 100644 test/setup.js diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..dc3280aa --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,3 @@ +{ + "require": "test/setup.js" +} diff --git a/test/acceptance/js/BrokenLatexFileTests.js b/test/acceptance/js/BrokenLatexFileTests.js index c26e0c40..34d3b37c 100644 --- a/test/acceptance/js/BrokenLatexFileTests.js +++ b/test/acceptance/js/BrokenLatexFileTests.js @@ -10,7 +10,6 @@ */ const Client = require('./helpers/Client') const request = require('request') -require('chai').should() const ClsiApp = require('./helpers/ClsiApp') describe('Broken LaTeX file', function () { diff --git a/test/acceptance/js/DeleteOldFilesTest.js b/test/acceptance/js/DeleteOldFilesTest.js index ae2d7c79..064f6be6 100644 --- a/test/acceptance/js/DeleteOldFilesTest.js +++ b/test/acceptance/js/DeleteOldFilesTest.js @@ -10,7 +10,6 @@ */ const Client = require('./helpers/Client') const request = require('request') -require('chai').should() const ClsiApp = require('./helpers/ClsiApp') describe('Deleting Old Files', function () { diff --git a/test/acceptance/js/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js index 5a67a0fe..92bab4b6 100644 --- a/test/acceptance/js/ExampleDocumentTests.js +++ b/test/acceptance/js/ExampleDocumentTests.js @@ -17,7 +17,6 @@ */ const Client = require('./helpers/Client') const request = require('request') -require('chai').should() const fs = require('fs') const fsExtra = require('fs-extra') const ChildProcess = require('child_process') diff --git a/test/acceptance/js/SimpleLatexFileTests.js b/test/acceptance/js/SimpleLatexFileTests.js index dd458026..7377afda 100644 --- a/test/acceptance/js/SimpleLatexFileTests.js +++ b/test/acceptance/js/SimpleLatexFileTests.js @@ -10,7 +10,6 @@ */ const Client = require('./helpers/Client') const request = require('request') -require('chai').should() const ClsiApp = require('./helpers/ClsiApp') describe('Simple LaTeX file', function () { diff --git a/test/acceptance/js/SynctexTests.js b/test/acceptance/js/SynctexTests.js index afe6330d..e636912b 100644 --- a/test/acceptance/js/SynctexTests.js +++ b/test/acceptance/js/SynctexTests.js @@ -11,7 +11,6 @@ */ const Client = require('./helpers/Client') const request = require('request') -require('chai').should() const { expect } = require('chai') const ClsiApp = require('./helpers/ClsiApp') const crypto = require('crypto') diff --git a/test/acceptance/js/TimeoutTests.js b/test/acceptance/js/TimeoutTests.js index 36bcf9aa..0d359bec 100644 --- a/test/acceptance/js/TimeoutTests.js +++ b/test/acceptance/js/TimeoutTests.js @@ -10,7 +10,6 @@ */ const Client = require('./helpers/Client') const request = require('request') -require('chai').should() const ClsiApp = require('./helpers/ClsiApp') describe('Timed out compile', function () { diff --git a/test/acceptance/js/UrlCachingTests.js b/test/acceptance/js/UrlCachingTests.js index 03927ea1..9528efba 100644 --- a/test/acceptance/js/UrlCachingTests.js +++ b/test/acceptance/js/UrlCachingTests.js @@ -11,7 +11,6 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const Client = require('./helpers/Client') -require('chai').should() const sinon = require('sinon') const ClsiApp = require('./helpers/ClsiApp') diff --git a/test/acceptance/js/WordcountTests.js b/test/acceptance/js/WordcountTests.js index 73a11f76..837eb47a 100644 --- a/test/acceptance/js/WordcountTests.js +++ b/test/acceptance/js/WordcountTests.js @@ -11,7 +11,6 @@ */ const Client = require('./helpers/Client') const request = require('request') -require('chai').should() const { expect } = require('chai') const path = require('path') const fs = require('fs') diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 00000000..87532f0a --- /dev/null +++ b/test/setup.js @@ -0,0 +1,19 @@ +const chai = require('chai') +const SandboxedModule = require('sandboxed-module') + +// Setup should interface +chai.should() + +// Global SandboxedModule settings +SandboxedModule.configure({ + requires: { + 'logger-sharelatex': { + log() {}, + info() {}, + warn() {}, + error() {}, + err() {} + } + }, + globals: { Buffer, console, process } +}) diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 70f67b60..d8c7c126 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -11,7 +11,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const { expect } = require('chai') const modulePath = require('path').join( __dirname, @@ -32,13 +31,7 @@ describe('CompileController', function () { } } }), - './ProjectPersistenceManager': (this.ProjectPersistenceManager = {}), - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - error: sinon.stub(), - err: sinon.stub(), - warn: sinon.stub() - }) + './ProjectPersistenceManager': (this.ProjectPersistenceManager = {}) } }) this.Settings.externalUrl = 'http://www.example.com' diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index 917dfe8c..e33783a1 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -15,7 +15,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/CompileManager' @@ -47,7 +46,6 @@ describe('CompileManager', function () { } }), - 'logger-sharelatex': (this.logger = { log: sinon.stub(), info() {} }), child_process: (this.child_process = {}), './CommandRunner': (this.CommandRunner = {}), './DraftModeManager': (this.DraftModeManager = {}), diff --git a/test/unit/js/ContentTypeMapperTests.js b/test/unit/js/ContentTypeMapperTests.js index 21173bc5..b6363623 100644 --- a/test/unit/js/ContentTypeMapperTests.js +++ b/test/unit/js/ContentTypeMapperTests.js @@ -12,7 +12,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/ContentTypeMapper' diff --git a/test/unit/js/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js index 19c289a1..6adfa123 100644 --- a/test/unit/js/DockerLockManagerTests.js +++ b/test/unit/js/DockerLockManagerTests.js @@ -11,7 +11,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/DockerLockManager' @@ -21,11 +20,7 @@ describe('LockManager', function () { beforeEach(function () { return (this.LockManager = SandboxedModule.require(modulePath, { requires: { - 'settings-sharelatex': (this.Settings = { clsi: { docker: {} } }), - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - error: sinon.stub() - }) + 'settings-sharelatex': (this.Settings = { clsi: { docker: {} } }) } })) }) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 517179a9..67290e44 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -15,7 +15,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const { expect } = require('chai') const modulePath = require('path').join( __dirname, @@ -33,12 +32,6 @@ describe('DockerRunner', function () { clsi: { docker: {} }, path: {} }), - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - error: sinon.stub(), - info: sinon.stub(), - warn: sinon.stub() - }), dockerode: (Docker = (function () { Docker = class Docker { static initClass() { diff --git a/test/unit/js/DraftModeManagerTests.js b/test/unit/js/DraftModeManagerTests.js index 937cde90..13291e02 100644 --- a/test/unit/js/DraftModeManagerTests.js +++ b/test/unit/js/DraftModeManagerTests.js @@ -10,7 +10,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/DraftModeManager' @@ -20,8 +19,7 @@ describe('DraftModeManager', function () { beforeEach(function () { return (this.DraftModeManager = SandboxedModule.require(modulePath, { requires: { - fs: (this.fs = {}), - 'logger-sharelatex': (this.logger = { log() {} }) + fs: (this.fs = {}) } })) }) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index 9cd0d0ac..a9116d0f 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -11,7 +11,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/LatexRunner' @@ -28,10 +27,6 @@ describe('LatexRunner', function () { socketPath: '/var/run/docker.sock' } }), - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - error: sinon.stub() - }), './Metrics': { Timer: (Timer = class Timer { done() {} diff --git a/test/unit/js/LockManagerTests.js b/test/unit/js/LockManagerTests.js index 16a43ade..58580583 100644 --- a/test/unit/js/LockManagerTests.js +++ b/test/unit/js/LockManagerTests.js @@ -11,7 +11,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/LockManager' @@ -24,11 +23,6 @@ describe('DockerLockManager', function () { this.LockManager = SandboxedModule.require(modulePath, { requires: { 'settings-sharelatex': {}, - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - error: sinon.stub(), - err() {} - }), fs: { lstat: sinon.stub().callsArgWith(1), readdir: sinon.stub().callsArgWith(1) diff --git a/test/unit/js/OutputFileFinderTests.js b/test/unit/js/OutputFileFinderTests.js index 8744c6e7..d094f7c6 100644 --- a/test/unit/js/OutputFileFinderTests.js +++ b/test/unit/js/OutputFileFinderTests.js @@ -12,7 +12,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/OutputFileFinder' @@ -26,8 +25,7 @@ describe('OutputFileFinder', function () { this.OutputFileFinder = SandboxedModule.require(modulePath, { requires: { fs: (this.fs = {}), - child_process: { spawn: (this.spawn = sinon.stub()) }, - 'logger-sharelatex': { log: sinon.stub(), warn: sinon.stub() } + child_process: { spawn: (this.spawn = sinon.stub()) } }, globals: { Math // used by lodash diff --git a/test/unit/js/OutputFileOptimiserTests.js b/test/unit/js/OutputFileOptimiserTests.js index e7f7b8b0..4c786660 100644 --- a/test/unit/js/OutputFileOptimiserTests.js +++ b/test/unit/js/OutputFileOptimiserTests.js @@ -12,7 +12,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/OutputFileOptimiser' @@ -28,7 +27,6 @@ describe('OutputFileOptimiser', function () { fs: (this.fs = {}), path: (this.Path = {}), child_process: { spawn: (this.spawn = sinon.stub()) }, - 'logger-sharelatex': { log: sinon.stub(), warn: sinon.stub() }, './Metrics': {} }, globals: { Math } // used by lodash diff --git a/test/unit/js/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js index 5e4368fd..5fb18272 100644 --- a/test/unit/js/ProjectPersistenceManagerTests.js +++ b/test/unit/js/ProjectPersistenceManagerTests.js @@ -13,7 +13,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const assert = require('chai').assert const modulePath = require('path').join( __dirname, @@ -28,11 +27,6 @@ describe('ProjectPersistenceManager', function () { './UrlCache': (this.UrlCache = {}), './CompileManager': (this.CompileManager = {}), diskusage: (this.diskusage = { check: sinon.stub() }), - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - warn: sinon.stub(), - err: sinon.stub() - }), 'settings-sharelatex': (this.settings = { project_cache_length_ms: 1000 }), diff --git a/test/unit/js/RequestParserTests.js b/test/unit/js/RequestParserTests.js index 6e6c9225..ff2ef7a9 100644 --- a/test/unit/js/RequestParserTests.js +++ b/test/unit/js/RequestParserTests.js @@ -11,7 +11,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const { expect } = require('chai') const modulePath = require('path').join( __dirname, diff --git a/test/unit/js/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js index e2bc0738..1536299c 100644 --- a/test/unit/js/ResourceStateManagerTests.js +++ b/test/unit/js/ResourceStateManagerTests.js @@ -12,7 +12,6 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const { expect } = require('chai') -const should = require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/ResourceStateManager' @@ -26,7 +25,6 @@ describe('ResourceStateManager', function () { singleOnly: true, requires: { fs: (this.fs = {}), - 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, './SafeReader': (this.SafeReader = {}) } }) diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 030fe70e..267fc2c3 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -13,7 +13,6 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const { expect } = require('chai') -const should = require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/ResourceWriter' @@ -34,7 +33,6 @@ describe('ResourceWriter', function () { wrench: (this.wrench = {}), './UrlCache': (this.UrlCache = {}), './OutputFileFinder': (this.OutputFileFinder = {}), - 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, './Metrics': (this.Metrics = { inc: sinon.stub(), Timer: (Timer = (function () { @@ -407,7 +405,7 @@ describe('ResourceWriter', function () { }) return it('should not return an error if the resource writer errored', function () { - return should.not.exist(this.callback.args[0][0]) + return expect(this.callback.args[0][0]).not.to.exist }) }) diff --git a/test/unit/js/StaticServerForbidSymlinksTests.js b/test/unit/js/StaticServerForbidSymlinksTests.js index 9a72168f..65a66f3d 100644 --- a/test/unit/js/StaticServerForbidSymlinksTests.js +++ b/test/unit/js/StaticServerForbidSymlinksTests.js @@ -9,7 +9,6 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const should = require('chai').should() const SandboxedModule = require('sandboxed-module') const assert = require('assert') const path = require('path') @@ -32,11 +31,6 @@ describe('StaticServerForbidSymlinks', function () { this.ForbidSymlinks = SandboxedModule.require(modulePath, { requires: { 'settings-sharelatex': this.settings, - 'logger-sharelatex': { - log() {}, - warn() {}, - error() {} - }, fs: this.fs } }) diff --git a/test/unit/js/TikzManager.js b/test/unit/js/TikzManager.js index 4304edc0..30a38075 100644 --- a/test/unit/js/TikzManager.js +++ b/test/unit/js/TikzManager.js @@ -10,7 +10,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join( __dirname, '../../../app/js/TikzManager' @@ -22,8 +21,7 @@ describe('TikzManager', function () { requires: { './ResourceWriter': (this.ResourceWriter = {}), './SafeReader': (this.SafeReader = {}), - fs: (this.fs = {}), - 'logger-sharelatex': (this.logger = { log() {} }) + fs: (this.fs = {}) } })) }) diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index 023b92de..2b991245 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -12,7 +12,6 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') -require('chai').should() const modulePath = require('path').join(__dirname, '../../../app/js/UrlCache') const { EventEmitter } = require('events') @@ -25,7 +24,6 @@ describe('UrlCache', function () { requires: { './db': {}, './UrlFetcher': (this.UrlFetcher = {}), - 'logger-sharelatex': (this.logger = { log: sinon.stub() }), 'settings-sharelatex': (this.Settings = { path: { clsiCacheDir: '/cache/dir' } }), diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index 96ef4574..238e5d8a 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -11,7 +11,6 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const { expect } = require('chai') -require('chai').should() const modulePath = require('path').join(__dirname, '../../../app/js/UrlFetcher') const { EventEmitter } = require('events') @@ -25,10 +24,6 @@ describe('UrlFetcher', function () { defaults: (this.defaults = sinon.stub().returns((this.request = {}))) }, fs: (this.fs = {}), - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - error: sinon.stub() - }), 'settings-sharelatex': (this.settings = {}) } })) From d0a4803a3a781bf2f99cf59d458eafc563a69380 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <eric.mcsween@overleaf.com> Date: Fri, 12 Mar 2021 15:00:19 -0500 Subject: [PATCH 634/709] Upgrade to Node 12 --- .dockerignore | 1 + .nvmrc | 2 +- Dockerfile | 4 ++-- Makefile | 6 ++++-- buildscript.txt | 6 +++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.dockerignore b/.dockerignore index 74fdc35e..bcbd7584 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ nodemon.json cache/ compiles/ db/ +output/ diff --git a/.nvmrc b/.nvmrc index 2baa2d43..e68b8603 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.23.1 +12.21.0 diff --git a/Dockerfile b/Dockerfile index 7eaa03f8..b02e828a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -FROM node:10.23.1 as base +FROM node:12.21.0 as base WORKDIR /app COPY install_deps.sh /app @@ -23,6 +23,6 @@ FROM base COPY --from=app /app /app RUN mkdir -p cache compiles db output \ - && chown node:node cache compiles db output +&& chown node:node cache compiles db output CMD ["node", "--expose-gc", "app.js"] diff --git a/Makefile b/Makefile index 040a9315..45a4bc04 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,10 @@ DOCKER_COMPOSE_TEST_UNIT = \ COMPOSE_PROJECT_NAME=test_unit_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) clean: - docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + -docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + -docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + -$(DOCKER_COMPOSE_TEST_UNIT) down --rmi local + -$(DOCKER_COMPOSE_TEST_ACCEPTANCE) down --rmi local format: $(DOCKER_COMPOSE) run --rm test_unit npm run --silent format diff --git a/buildscript.txt b/buildscript.txt index b2093150..a86911dc 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -1,9 +1,9 @@ clsi ---data-dirs=cache,compiles,db +--data-dirs=cache,compiles,db,output --dependencies= --docker-repos=gcr.io/overleaf-ops --env-add= --env-pass-through=TEXLIVE_IMAGE ---node-version=10.23.1 +--node-version=12.21.0 --public-repo=True ---script-version=3.4.0 +--script-version=3.7.0 From f85fa3314a0bc73ffe134cebdf5f1546fc005612 Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Tue, 16 Mar 2021 10:24:59 +0000 Subject: [PATCH 635/709] support post and get for getting status of clsi project --- app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app.js b/app.js index aee8d448..f24ce5f9 100644 --- a/app.js +++ b/app.js @@ -87,6 +87,7 @@ app.get('/project/:project_id/sync/code', CompileController.syncFromCode) app.get('/project/:project_id/sync/pdf', CompileController.syncFromPdf) app.get('/project/:project_id/wordcount', CompileController.wordcount) app.get('/project/:project_id/status', CompileController.status) +app.post('/project/:project_id/status', CompileController.status) // Per-user containers app.post( From 889d2472d60901dd07c965edbf0fee2c00fd163a Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 16 Mar 2021 12:00:48 +0000 Subject: [PATCH 636/709] Expand list of environment variables --- README.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c4c1eabf..6f67355b 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,29 @@ The provided `Dockerfile` builds a docker image which has the docker command lin The CLSI can be configured through the following environment variables: - * `DOCKER_RUNNER` - Set to true to use sibling containers - * `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary - * `COMPILES_HOST_DIR` - Working directory for LaTeX compiles - * `SQLITE_PATH` - Path to SQLite database - * `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` - * `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` - * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` - * `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` - * `STATSD_HOST` - The address of the Statsd service (used by the metrics module) - * `LISTEN_ADDRESS` - The address for the RESTful service to listen on. Set to `0.0.0.0` to listen on all network interfaces - * `SMOKE_TEST` - Whether to run smoke tests +* `ALLOWED_COMPILE_GROUPS` +* `ALLOWED_IMAGES` +* `CATCH_ERRORS` +* `COMPILE_GROUP_DOCKER_CONFIGS` +* `COMPILES_HOST_DIR` - Working directory for LaTeX compiles +* `COMPILE_SIZE_LIMIT` +* `DOCKER_RUNNER` - Set to true to use sibling containers +* `DOCKER_RUNTIME` +* `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` +* `FILESTORE_PARALLEL_FILE_DOWNLOADS` +* `FILESTORE_PARALLEL_SQL_QUERY_LIMIT` +* `LISTEN_ADDRESS` - The address for the RESTful service to listen on. Set to `0.0.0.0` to listen on all network interfaces +* `PROCESS_LIFE_SPAN_LIMIT_MS` +* `SENTRY_DSN` +* `SMOKE_TEST` - Whether to run smoke tests +* `SQLITE_PATH` - Path to SQLite database +* `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary +* `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` +* `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` +* `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` +* `TEXLIVE_OPENOUT_ANY` + +Further environment variables configure the [metrics module](https://github.com/overleaf/metrics-module) Installation ------------ From 671184363c9cc778c470cc42cc55ec0ed0a101f4 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 16 Mar 2021 12:30:06 +0000 Subject: [PATCH 637/709] Fill in missing text for environment variables --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6f67355b..b82efa2b 100644 --- a/README.md +++ b/README.md @@ -15,27 +15,27 @@ The provided `Dockerfile` builds a docker image which has the docker command lin The CLSI can be configured through the following environment variables: -* `ALLOWED_COMPILE_GROUPS` -* `ALLOWED_IMAGES` -* `CATCH_ERRORS` -* `COMPILE_GROUP_DOCKER_CONFIGS` +* `ALLOWED_COMPILE_GROUPS` - Space separated list of allowed compile groups +* `ALLOWED_IMAGES` - Space separated list of allowed Docker TeX Live images +* `CATCH_ERRORS` - Set to `true` to log uncaught exceptions +* `COMPILE_GROUP_DOCKER_CONFIGS` - JSON string of Docker configs for compile groups * `COMPILES_HOST_DIR` - Working directory for LaTeX compiles -* `COMPILE_SIZE_LIMIT` +* `COMPILE_SIZE_LIMIT` - Sets the body-parser [limit](https://github.com/expressjs/body-parser#limit) * `DOCKER_RUNNER` - Set to true to use sibling containers -* `DOCKER_RUNTIME` +* `DOCKER_RUNTIME` - * `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` -* `FILESTORE_PARALLEL_FILE_DOWNLOADS` -* `FILESTORE_PARALLEL_SQL_QUERY_LIMIT` +* `FILESTORE_PARALLEL_FILE_DOWNLOADS` - Number of parallel file downloads +* `FILESTORE_PARALLEL_SQL_QUERY_LIMIT` - Number of parallel SQL queries * `LISTEN_ADDRESS` - The address for the RESTful service to listen on. Set to `0.0.0.0` to listen on all network interfaces -* `PROCESS_LIFE_SPAN_LIMIT_MS` -* `SENTRY_DSN` +* `PROCESS_LIFE_SPAN_LIMIT_MS` - Process life span limit in milliseconds +* `SENTRY_DSN` - Sentry [Data Source Name](https://docs.sentry.io/product/sentry-basics/dsn-explainer/) * `SMOKE_TEST` - Whether to run smoke tests * `SQLITE_PATH` - Path to SQLite database * `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary * `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` * `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` -* `TEXLIVE_OPENOUT_ANY` +* `TEXLIVE_OPENOUT_ANY` - Sets the `openout_any` environment variable for Tex Live Further environment variables configure the [metrics module](https://github.com/overleaf/metrics-module) From 14ece16bcb4c99f05be50eb0793086a5ac67f01d Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Tue, 16 Mar 2021 15:29:53 +0000 Subject: [PATCH 638/709] README typos --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b82efa2b..54f6e0a8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The Common LaTeX Service Interface (CLSI) provides a RESTful interface to tradit These defaults can be modified in `config/settings.defaults.js`. -The provided `Dockerfile` builds a docker image which has the docker command line tools installed. The configuration in `docker-compose-config.yml` mounts the docker socket, in order that the CLSI container can talk to the docker host it is running in. This allows it to spin up `sibling containers` running an image with a TeX distribution installed to perform the actual compiles. +The provided `Dockerfile` builds a Docker image which has the Docker command line tools installed. The configuration in `docker-compose-config.yml` mounts the Docker socket, in order that the CLSI container can talk to the Docker host it is running in. This allows it to spin up `sibling containers` running an image with a TeX distribution installed to perform the actual compiles. The CLSI can be configured through the following environment variables: @@ -32,10 +32,10 @@ The CLSI can be configured through the following environment variables: * `SMOKE_TEST` - Whether to run smoke tests * `SQLITE_PATH` - Path to SQLite database * `SYNCTEX_BIN_HOST_PATH` - Path to SyncTeX binary -* `TEXLIVE_IMAGE` - The TEXLIVE docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` -* `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the docker image e.g. `gcr.io/overleaf-ops` -* `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TEXLIVE image. Defaults to `tex` -* `TEXLIVE_OPENOUT_ANY` - Sets the `openout_any` environment variable for Tex Live +* `TEXLIVE_IMAGE` - The TeX Live Docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` +* `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the Docker image e.g. `gcr.io/overleaf-ops` +* `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TeX Live image. Defaults to `tex` +* `TEXLIVE_OPENOUT_ANY` - Sets the `openout_any` environment variable for TeX Live Further environment variables configure the [metrics module](https://github.com/overleaf/metrics-module) @@ -50,7 +50,7 @@ Then build the Docker image: $ docker build . -t overleaf/clsi -Then pull the TeXLive image: +Then pull the TeX Live image: $ docker pull texlive/texlive From ab6fe228cadd3329885fdc60aea867d601579759 Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Thu, 18 Mar 2021 09:55:31 +0000 Subject: [PATCH 639/709] Make TEXLIVE_IMAGE_USER instruction macOS only --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 54f6e0a8..90dee749 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,13 @@ Then start the Docker container: -e LISTEN_ADDRESS=0.0.0.0 \ -e DOCKER_RUNNER=true \ -e TEXLIVE_IMAGE=texlive/texlive \ - -e TEXLIVE_IMAGE_USER=root \ -e COMPILES_HOST_DIR="$PWD/compiles" \ -v "$PWD/compiles:/app/compiles" \ -v "$PWD/cache:/app/cache" \ -v /var/run/docker.sock:/var/run/docker.sock \ overleaf/clsi -Note: if you're running the CLSI in macOS you may need to use `-v /var/run/docker.sock.raw:/var/run/docker.sock` instead. +Note: if you're running the CLSI in macOS you may need to add `-e TEXLIVE_IMAGE_USER=root` and use `-v /var/run/docker.sock.raw:/var/run/docker.sock` instead. The CLSI should then be running at <http://localhost:3013> From 1dd74067aaa56db8231a4ac64862c9ca39798246 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 19 Mar 2021 10:34:52 +0000 Subject: [PATCH 640/709] Add link to \openout primitive docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54f6e0a8..e822d92e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The CLSI can be configured through the following environment variables: * `TEXLIVE_IMAGE` - The TeX Live Docker image to use for sibling containers, e.g. `gcr.io/overleaf-ops/texlive-full:2017.1` * `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the Docker image e.g. `gcr.io/overleaf-ops` * `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TeX Live image. Defaults to `tex` -* `TEXLIVE_OPENOUT_ANY` - Sets the `openout_any` environment variable for TeX Live +* `TEXLIVE_OPENOUT_ANY` - Sets the `openout_any` environment variable for TeX Live (see the `\openout` primitive [documentation](http://tug.org/texinfohtml/web2c.html#tex-invocation)) Further environment variables configure the [metrics module](https://github.com/overleaf/metrics-module) From 41438a369864dc5ab0508ff0c8e31d6dd0c51bf2 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly <shane.kilkelly@overleaf.com> Date: Fri, 19 Mar 2021 11:35:59 +0000 Subject: [PATCH 641/709] Add flag to qpdf, to preserve PDF/A compliance --- app/js/OutputFileOptimiser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/js/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js index 85273762..0b835a42 100644 --- a/app/js/OutputFileOptimiser.js +++ b/app/js/OutputFileOptimiser.js @@ -71,7 +71,7 @@ module.exports = OutputFileOptimiser = { callback = function (error) {} } const tmpOutput = dst + '.opt' - const args = ['--linearize', src, tmpOutput] + const args = ['--linearize', '--newline-before-endstream', src, tmpOutput] logger.log({ args }, 'running qpdf command') const timer = new Metrics.Timer('qpdf') From f179669cbc3d1bd8422cb0a8ef26cbde75fa58ef Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Fri, 19 Mar 2021 12:00:47 +0000 Subject: [PATCH 642/709] Revert "Make TEXLIVE_IMAGE_USER instruction macOS only" This reverts commit ab6fe228cadd3329885fdc60aea867d601579759. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 416d773f..e822d92e 100644 --- a/README.md +++ b/README.md @@ -61,13 +61,14 @@ Then start the Docker container: -e LISTEN_ADDRESS=0.0.0.0 \ -e DOCKER_RUNNER=true \ -e TEXLIVE_IMAGE=texlive/texlive \ + -e TEXLIVE_IMAGE_USER=root \ -e COMPILES_HOST_DIR="$PWD/compiles" \ -v "$PWD/compiles:/app/compiles" \ -v "$PWD/cache:/app/cache" \ -v /var/run/docker.sock:/var/run/docker.sock \ overleaf/clsi -Note: if you're running the CLSI in macOS you may need to add `-e TEXLIVE_IMAGE_USER=root` and use `-v /var/run/docker.sock.raw:/var/run/docker.sock` instead. +Note: if you're running the CLSI in macOS you may need to use `-v /var/run/docker.sock.raw:/var/run/docker.sock` instead. The CLSI should then be running at <http://localhost:3013> From 6253195c71600b0b1a5e2ecb47a7817938c7f206 Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Fri, 19 Mar 2021 12:05:22 +0000 Subject: [PATCH 643/709] Add instructions for Linux --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e822d92e..d8e189dc 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ The provided `Dockerfile` builds a Docker image which has the Docker command lin The CLSI can be configured through the following environment variables: * `ALLOWED_COMPILE_GROUPS` - Space separated list of allowed compile groups -* `ALLOWED_IMAGES` - Space separated list of allowed Docker TeX Live images +* `ALLOWED_IMAGES` - Space separated list of allowed Docker TeX Live images * `CATCH_ERRORS` - Set to `true` to log uncaught exceptions * `COMPILE_GROUP_DOCKER_CONFIGS` - JSON string of Docker configs for compile groups * `COMPILES_HOST_DIR` - Working directory for LaTeX compiles * `COMPILE_SIZE_LIMIT` - Sets the body-parser [limit](https://github.com/expressjs/body-parser#limit) * `DOCKER_RUNNER` - Set to true to use sibling containers -* `DOCKER_RUNTIME` - +* `DOCKER_RUNTIME` - * `FILESTORE_DOMAIN_OVERRIDE` - The url for the filestore service e.g.`http://$FILESTORE_HOST:3009` * `FILESTORE_PARALLEL_FILE_DOWNLOADS` - Number of parallel file downloads * `FILESTORE_PARALLEL_SQL_QUERY_LIMIT` - Number of parallel SQL queries @@ -70,6 +70,13 @@ Then start the Docker container: Note: if you're running the CLSI in macOS you may need to use `-v /var/run/docker.sock.raw:/var/run/docker.sock` instead. +Note: if you're running the CLSI in Linux you may need to adjust the permissions of the `compiles` folder to match your local user: + +```shell +sudo chown -R $(whoami):root compiles/ +sudo chmod g+w -R compiles/ +``` + The CLSI should then be running at <http://localhost:3013> Config From cb966e7afcdc91cb9cc1cec67f4f7112f95b742d Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <christopher.hoskin@gmail.com> Date: Fri, 19 Mar 2021 14:32:54 +0000 Subject: [PATCH 644/709] Explain the situation with permissions on Linux --- README.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d8e189dc..302cb349 100644 --- a/README.md +++ b/README.md @@ -66,18 +66,45 @@ Then start the Docker container: -v "$PWD/compiles:/app/compiles" \ -v "$PWD/cache:/app/cache" \ -v /var/run/docker.sock:/var/run/docker.sock \ + --name clsi \ overleaf/clsi Note: if you're running the CLSI in macOS you may need to use `-v /var/run/docker.sock.raw:/var/run/docker.sock` instead. -Note: if you're running the CLSI in Linux you may need to adjust the permissions of the `compiles` folder to match your local user: +The CLSI should then be running at <http://localhost:3013> + +Important note for Linux users +============================== -```shell -sudo chown -R $(whoami):root compiles/ -sudo chmod g+w -R compiles/ +The Node application runs as user `node` in the CLSI, which has uid `1000`. As a consequence of this, the `compiles` folder gets created on your host with `uid` and `gid` set to `1000`. +``` +ls -lnd compiles +drwxr-xr-x 2 1000 1000 4096 Mar 19 12:41 compiles ``` -The CLSI should then be running at <http://localhost:3013> +If there is a user/group on your host which also happens to have `uid` / `gid` `1000` then that user/group will have ownership of the compiles folder on your host. + +LaTeX runs in the sibling containers as the user specified in the `TEXLIVE_IMAGE_USER` environment variable. In the example above this is set to `root`, which has uid `0`. This creates a problem with the above permissions, as the root user does not have permission to write to subfolders of `compiles`. + +A quick fix is to give the `root` group ownership and read write permissions to `compiles`, with `setgid` set so that new subfolders also inherit this ownership: +``` +sudo chown -R 1000:root compiles +sudo chmod -R g+w compiles +sudo chmod g+s compiles +``` +Another solution is to create a `sharelatex` group and add both `root` and the user with `uid` `1000` to it. If the host does not have a user with that `uid`, you will need to create one first. +``` +sudo useradd --uid 1000 host-node-user # If required +sudo groupadd sharelatex +sudo usermod -a -G sharelatex root +sudo usermod -a -G sharelatex $(id -nu 1000) +sudo chown -R 1000:sharelatex compiles +sudo chmod -R g+w compiles +sudo chmod g+s compiles +``` + +This is a facet of the way docker works on Linux. See this [upstream issue](https://github.com/moby/moby/issues/7198) + Config ------ From 6149424d9631794be0b1de047e8caed282aa5bcb Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 16 Feb 2021 15:10:09 +0000 Subject: [PATCH 645/709] [misc] bump the version of the metrics module to 3.5.1 --- package-lock.json | 891 ++++++++++++++++++++++++++++++++-------------- package.json | 4 +- 2 files changed, 616 insertions(+), 279 deletions(-) diff --git a/package-lock.json b/package-lock.json index 145ec532..a4fb21c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -215,6 +215,202 @@ } } }, + "@google-cloud/debug-agent": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-5.1.3.tgz", + "integrity": "sha512-WbzeEz4MvPlM7DX2QBsPcWgF62u7LSQv/oMYPl0L+TddTebqjDKiVXwxpzWk61NIfcKiet3dyCbPIt3N5o8XPQ==", + "requires": { + "@google-cloud/common": "^3.0.0", + "acorn": "^8.0.0", + "coffeescript": "^2.0.0", + "console-log-level": "^1.4.0", + "extend": "^3.0.2", + "findit2": "^2.2.3", + "gcp-metadata": "^4.0.0", + "p-limit": "^3.0.1", + "semver": "^7.0.0", + "source-map": "^0.6.1", + "split": "^1.0.0" + }, + "dependencies": { + "@google-cloud/common": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz", + "integrity": "sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==", + "requires": { + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^6.1.1", + "retry-request": "^4.1.1", + "teeny-request": "^7.0.0" + } + }, + "@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==" + }, + "@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" + }, + "acorn": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz", + "integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==" + }, + "bignumber.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" + }, + "coffeescript": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", + "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" + }, + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "gaxios": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", + "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", + "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", + "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "requires": { + "node-forge": "^0.10.0" + } + }, + "gtoken": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", + "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "teeny-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", + "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "@google-cloud/logging": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-7.3.0.tgz", @@ -301,81 +497,247 @@ "extend": "^3.0.2" } }, - "@google-cloud/projectify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", - "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==" - }, - "@google-cloud/promisify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", - "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==" - }, - "@grpc/grpc-js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", - "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", - "requires": { - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "@grpc/proto-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", - "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", - "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - } - }, - "@opencensus/core": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.20.tgz", - "integrity": "sha512-vqOuTd2yuMpKohp8TNNGUAPjWEGjlnGfB9Rh5e3DKqeyR94YgierNs4LbMqxKtsnwB8Dm2yoEtRuUgoe5vD9DA==", + "@google-cloud/profiler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-4.1.0.tgz", + "integrity": "sha512-9e1zXRctLSUHAoAsFGwE4rS28fr0siiG+jXl5OpwTK8ZAUlxb70aosHaZGdsv8YXrYKjuiufjRZ/OXCs0XLI9g==", "requires": { - "continuation-local-storage": "^3.2.1", - "log-driver": "^1.2.7", - "semver": "^6.0.0", - "shimmer": "^1.2.0", - "uuid": "^3.2.1" + "@google-cloud/common": "^3.0.0", + "@types/console-log-level": "^1.4.0", + "@types/semver": "^7.0.0", + "console-log-level": "^1.4.0", + "delay": "^4.0.1", + "extend": "^3.0.2", + "gcp-metadata": "^4.0.0", + "parse-duration": "^0.4.4", + "pprof": "3.0.0", + "pretty-ms": "^7.0.0", + "protobufjs": "~6.10.0", + "semver": "^7.0.0", + "teeny-request": "^7.0.0" }, "dependencies": { + "@google-cloud/common": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz", + "integrity": "sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==", + "requires": { + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^6.1.1", + "retry-request": "^4.1.1", + "teeny-request": "^7.0.0" + } + }, + "@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==" + }, + "@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/node": { + "version": "13.13.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.42.tgz", + "integrity": "sha512-g+w2QgbW7k2CWLOXzQXbO37a7v5P9ObPvYahKphdBLV5aqpbVZRhTpWCT0SMRqX1i30Aig791ZmIM2fJGL2S8A==" + }, + "bignumber.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" + }, + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "gaxios": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", + "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", + "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", + "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "requires": { + "node-forge": "^0.10.0" + } + }, + "gtoken": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", + "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "protobufjs": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", + "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "teeny-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", + "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, - "@opencensus/propagation-stackdriver": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.20.tgz", - "integrity": "sha512-P8yuHSLtce+yb+2EZjtTVqG7DQ48laC+IuOWi3X9q78s1Gni5F9+hmbmyP6Nb61jb5BEvXQX1s2rtRI6bayUWA==", - "requires": { - "@opencensus/core": "^0.0.20", - "hex2dec": "^1.0.1", - "uuid": "^3.2.1" - } - }, - "@overleaf/metrics": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@overleaf/metrics/-/metrics-3.4.1.tgz", - "integrity": "sha512-OgjlzuC+2gPdIEDHhmd9LDMu01tk1ln0cJhw1727BZ+Wgf2Z1hjuHRt4JeCkf+PFTHwJutVYT8v6IGPpNEPtbg==", + "@google-cloud/projectify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", + "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==" + }, + "@google-cloud/promisify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", + "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==" + }, + "@google-cloud/trace-agent": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-5.1.3.tgz", + "integrity": "sha512-f+5DX7n6QpDlHA+4kr81z69SLAdrlvd9T8skqCMgnYvtXx14AwzXZyzEDf3jppOYzYoqPPJv8XYiyYHHmYD0BA==", "requires": { - "@google-cloud/debug-agent": "^5.1.2", - "@google-cloud/profiler": "^4.0.3", - "@google-cloud/trace-agent": "^5.1.1", - "compression": "^1.7.4", - "prom-client": "^11.1.3", - "underscore": "~1.6.0", - "yn": "^3.1.1" + "@google-cloud/common": "^3.0.0", + "@opencensus/propagation-stackdriver": "0.0.22", + "builtin-modules": "^3.0.0", + "console-log-level": "^1.4.0", + "continuation-local-storage": "^3.2.1", + "extend": "^3.0.2", + "gcp-metadata": "^4.0.0", + "google-auth-library": "^7.0.0", + "hex2dec": "^1.0.1", + "is": "^3.2.0", + "methods": "^1.1.1", + "require-in-the-middle": "^5.0.0", + "semver": "^7.0.0", + "shimmer": "^1.2.0", + "source-map-support": "^0.5.16", + "uuid": "^8.0.0" }, "dependencies": { "@google-cloud/common": { @@ -392,44 +754,24 @@ "google-auth-library": "^6.1.1", "retry-request": "^4.1.1", "teeny-request": "^7.0.0" - } - }, - "@google-cloud/debug-agent": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@google-cloud/debug-agent/-/debug-agent-5.1.3.tgz", - "integrity": "sha512-WbzeEz4MvPlM7DX2QBsPcWgF62u7LSQv/oMYPl0L+TddTebqjDKiVXwxpzWk61NIfcKiet3dyCbPIt3N5o8XPQ==", - "requires": { - "@google-cloud/common": "^3.0.0", - "acorn": "^8.0.0", - "coffeescript": "^2.0.0", - "console-log-level": "^1.4.0", - "extend": "^3.0.2", - "findit2": "^2.2.3", - "gcp-metadata": "^4.0.0", - "p-limit": "^3.0.1", - "semver": "^7.0.0", - "source-map": "^0.6.1", - "split": "^1.0.0" - } - }, - "@google-cloud/profiler": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@google-cloud/profiler/-/profiler-4.1.0.tgz", - "integrity": "sha512-9e1zXRctLSUHAoAsFGwE4rS28fr0siiG+jXl5OpwTK8ZAUlxb70aosHaZGdsv8YXrYKjuiufjRZ/OXCs0XLI9g==", - "requires": { - "@google-cloud/common": "^3.0.0", - "@types/console-log-level": "^1.4.0", - "@types/semver": "^7.0.0", - "console-log-level": "^1.4.0", - "delay": "^4.0.1", - "extend": "^3.0.2", - "gcp-metadata": "^4.0.0", - "parse-duration": "^0.4.4", - "pprof": "3.0.0", - "pretty-ms": "^7.0.0", - "protobufjs": "~6.10.0", - "semver": "^7.0.0", - "teeny-request": "^7.0.0" + }, + "dependencies": { + "google-auth-library": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", + "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + } } }, "@google-cloud/projectify": { @@ -442,29 +784,6 @@ "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" }, - "@google-cloud/trace-agent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-5.1.1.tgz", - "integrity": "sha512-YTcK0RLN90pLCprg0XC8uV4oAVd79vsXhkcxmEVwiOOYjUDvSrAhb7y/0SY606zgfhJHmUTNb/fZSWEtZP/slQ==", - "requires": { - "@google-cloud/common": "^3.0.0", - "@opencensus/propagation-stackdriver": "0.0.22", - "builtin-modules": "^3.0.0", - "console-log-level": "^1.4.0", - "continuation-local-storage": "^3.2.1", - "extend": "^3.0.2", - "gcp-metadata": "^4.0.0", - "google-auth-library": "^6.0.0", - "hex2dec": "^1.0.1", - "is": "^3.2.0", - "methods": "^1.1.1", - "require-in-the-middle": "^5.0.0", - "semver": "^7.0.0", - "shimmer": "^1.2.0", - "source-map-support": "^0.5.16", - "uuid": "^8.0.0" - } - }, "@opencensus/core": { "version": "0.0.22", "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.22.tgz", @@ -487,44 +806,11 @@ "uuid": "^8.0.0" } }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "@types/node": { - "version": "13.13.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.33.tgz", - "integrity": "sha512-1B3GM1yuYsFyEvBb+ljBqWBOylsWDYioZ5wpu8AhXdIhq20neXS7eaSC8GkwHE0yQYGiOIV43lMsgRYTgKZefQ==" - }, - "@types/semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==" - }, - "acorn": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz", - "integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==" - }, "bignumber.js": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" }, - "coffeescript": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", - "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, "duplexify": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", @@ -537,9 +823,9 @@ } }, "gaxios": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.0.1.tgz", - "integrity": "sha512-jOin8xRZ/UytQeBpSXFqIzqU7Fi5TqgPNLlUsSB8kjJ76+FiGBfImF8KJu++c6J4jOldfJUtt0YmkRj2ZpSHTQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", @@ -558,9 +844,9 @@ } }, "google-auth-library": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.3.tgz", - "integrity": "sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.0.2.tgz", + "integrity": "sha512-vjyNZR3pDLC0u7GHLfj+Hw9tGprrJwoMwkYGqURCXYITjCrP9HprOyxVV+KekdLgATtWGuDkQG2MTh0qpUPUgg==", "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", @@ -582,14 +868,13 @@ } }, "gtoken": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.1.0.tgz", - "integrity": "sha512-4d8N6Lk8TEAHl9vVoRVMh9BNOKWVgl2DdNtr3428O75r3QFrF/a5MMu851VmK0AA8+iSvbwRv69k5XnMLURGhg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", + "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", "requires": { "gaxios": "^4.0.0", "google-p12-pem": "^3.0.3", - "jws": "^4.0.0", - "mime": "^2.2.0" + "jws": "^4.0.0" } }, "json-bigint": { @@ -600,25 +885,6 @@ "bignumber.js": "^9.0.0" } }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -627,62 +893,11 @@ "yallist": "^4.0.0" } }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "parse-duration": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.4.4.tgz", - "integrity": "sha512-KbAJuYGUhZkB9gotDiKLnZ7Z3VTacK3fgwmDdB6ZVDtJbMBT6MfLga0WJaYpPDu0mzqT0NgHtHDt5PY4l0nidg==" - }, - "pretty-ms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", - "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", - "requires": { - "parse-ms": "^2.1.0" - } - }, - "protobufjs": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", - "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": "^13.7.0", - "long": "^4.0.0" - } - }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -693,21 +908,14 @@ "util-deprecate": "^1.0.1" } }, - "require-in-the-middle": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.0.3.tgz", - "integrity": "sha512-p/ICV8uMlqC4tjOYabLMxAWCIKa0YUQgZZ6KDM0xgXJNgdGQ1WmL2A07TwmrZw+wi6ITUFKzH5v3n+ENEyXVkA==", + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "requires": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.12.0" + "lru-cache": "^6.0.0" } }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" - }, "teeny-request": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", @@ -721,9 +929,9 @@ } }, "uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "yallist": { "version": "4.0.0", @@ -732,6 +940,73 @@ } } }, + "@grpc/grpc-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz", + "integrity": "sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og==", + "requires": { + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@grpc/proto-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", + "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", + "requires": { + "lodash.camelcase": "^4.3.0", + "protobufjs": "^6.8.6" + } + }, + "@opencensus/core": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.20.tgz", + "integrity": "sha512-vqOuTd2yuMpKohp8TNNGUAPjWEGjlnGfB9Rh5e3DKqeyR94YgierNs4LbMqxKtsnwB8Dm2yoEtRuUgoe5vD9DA==", + "requires": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^6.0.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@opencensus/propagation-stackdriver": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-stackdriver/-/propagation-stackdriver-0.0.20.tgz", + "integrity": "sha512-P8yuHSLtce+yb+2EZjtTVqG7DQ48laC+IuOWi3X9q78s1Gni5F9+hmbmyP6Nb61jb5BEvXQX1s2rtRI6bayUWA==", + "requires": { + "@opencensus/core": "^0.0.20", + "hex2dec": "^1.0.1", + "uuid": "^3.2.1" + } + }, + "@overleaf/metrics": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@overleaf/metrics/-/metrics-3.5.1.tgz", + "integrity": "sha512-RLHxkMF7Y3725L3QwXo9cIn2gGobsMYUGuxKxg7PVMrPTMsomHEMeG7StOxCO7ML1Z/BwB/9nsVYNrsRdAJtKg==", + "requires": { + "@google-cloud/debug-agent": "^5.1.2", + "@google-cloud/profiler": "^4.0.3", + "@google-cloud/trace-agent": "^5.1.1", + "compression": "^1.7.4", + "prom-client": "^11.1.3", + "underscore": "~1.6.0", + "yn": "^3.1.1" + } + }, "@overleaf/o-error": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-3.0.0.tgz", @@ -898,6 +1173,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz", "integrity": "sha1-IOhWUbYv2GZW5Xycm8dxqxVwvFk=" }, + "@types/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==" + }, "@typescript-eslint/experimental-utils": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", @@ -1351,9 +1631,9 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "builtin-modules": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", - "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==" }, "bunyan": { "version": "1.8.12", @@ -1797,9 +2077,9 @@ } }, "delay": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz", - "integrity": "sha512-Lwaf3zVFDMBop1yDuFZ19F9WyGcZcGacsbdlZtWjQmM50tOcMntm1njF/Nb/Vjij3KaSvCF+sEYGKrrjObu2NA==" + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/delay/-/delay-4.4.1.tgz", + "integrity": "sha512-aL3AhqtfhOlT/3ai6sWXeqwnw63ATNpnUiN4HL7x9q+My5QtHlO3OIkasmug9LKzpheLdmUKGRKnYXYAS7FQkQ==" }, "delayed-stream": { "version": "1.0.0", @@ -3786,6 +4066,25 @@ "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", "dev": true }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -4700,6 +4999,11 @@ "callsites": "^3.0.0" } }, + "parse-duration": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.4.4.tgz", + "integrity": "sha512-KbAJuYGUhZkB9gotDiKLnZ7Z3VTacK3fgwmDdB6ZVDtJbMBT6MfLga0WJaYpPDu0mzqT0NgHtHDt5PY4l0nidg==" + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -4786,6 +5090,11 @@ "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", "dev": true }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, "pkg-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", @@ -4818,9 +5127,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "13.13.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.33.tgz", - "integrity": "sha512-1B3GM1yuYsFyEvBb+ljBqWBOylsWDYioZ5wpu8AhXdIhq20neXS7eaSC8GkwHE0yQYGiOIV43lMsgRYTgKZefQ==" + "version": "13.13.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.42.tgz", + "integrity": "sha512-g+w2QgbW7k2CWLOXzQXbO37a7v5P9ObPvYahKphdBLV5aqpbVZRhTpWCT0SMRqX1i30Aig791ZmIM2fJGL2S8A==" }, "debug": { "version": "3.2.7", @@ -4844,14 +5153,14 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "needle": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", - "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", + "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -4883,11 +5192,6 @@ "yocto-queue": "^0.1.0" } }, - "pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" - }, "protobufjs": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", @@ -5552,6 +5856,14 @@ } } }, + "pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "requires": { + "parse-ms": "^2.1.0" + } + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -5869,6 +6181,31 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-in-the-middle": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.1.0.tgz", + "integrity": "sha512-M2rLKVupQfJ5lf9OvqFGIT+9iVLnTmjgbOmpil12hiSQNn5zJTKGPoIisETNjfK+09vP3rpm1zJajmErpr2sEQ==", + "requires": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.12.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "require-like": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", diff --git a/package.json b/package.json index 8d74faac..eb194d9e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --exit --recursive --reporter spec $@ test/unit/js", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", "lint": "node_modules/.bin/eslint --max-warnings 0 .", @@ -19,7 +19,7 @@ }, "author": "James Allen <james@sharelatex.com>", "dependencies": { - "@overleaf/metrics": "^3.4.1", + "@overleaf/metrics": "^3.5.1", "async": "3.2.0", "body-parser": "^1.19.0", "diskusage": "^1.1.3", From 0b92ea4327211c5eadbf31f771a6108fe3bad4b3 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 30 Mar 2021 13:22:11 +0100 Subject: [PATCH 646/709] [misc] consume and validate a custom imageName for synctex requests --- app/js/CompileController.js | 27 ++++++-- app/js/CompileManager.js | 84 +++++++++++++--------- test/acceptance/js/AllowedImageNames.js | 75 +++++++++++++++++++- test/acceptance/js/SynctexTests.js | 16 ++--- test/acceptance/js/helpers/Client.js | 18 ++++- test/unit/js/CompileControllerTests.js | 92 ++++++++++++++----------- test/unit/js/CompileManagerTests.js | 76 +++++++++++++++++++- 7 files changed, 290 insertions(+), 98 deletions(-) diff --git a/app/js/CompileController.js b/app/js/CompileController.js index b0ce3bb6..ddf83d7c 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -21,6 +21,12 @@ const ProjectPersistenceManager = require('./ProjectPersistenceManager') const logger = require('logger-sharelatex') const Errors = require('./Errors') +function isImageNameAllowed(imageName) { + const ALLOWED_IMAGES = + Settings.clsi && Settings.clsi.docker && Settings.clsi.docker.allowedImages + return !ALLOWED_IMAGES || ALLOWED_IMAGES.includes(imageName) +} + module.exports = CompileController = { compile(req, res, next) { if (next == null) { @@ -165,14 +171,21 @@ module.exports = CompileController = { const { file } = req.query const line = parseInt(req.query.line, 10) const column = parseInt(req.query.column, 10) + const { imageName } = req.query const { project_id } = req.params const { user_id } = req.params + + if (imageName && !isImageNameAllowed(imageName)) { + return res.status(400).send('invalid image') + } + return CompileManager.syncFromCode( project_id, user_id, file, line, column, + imageName, function (error, pdfPositions) { if (error != null) { return next(error) @@ -191,14 +204,20 @@ module.exports = CompileController = { const page = parseInt(req.query.page, 10) const h = parseFloat(req.query.h) const v = parseFloat(req.query.v) + const { imageName } = req.query const { project_id } = req.params const { user_id } = req.params + + if (imageName && !isImageNameAllowed(imageName)) { + return res.status(400).send('invalid image') + } return CompileManager.syncFromPdf( project_id, user_id, page, h, v, + imageName, function (error, codePositions) { if (error != null) { return next(error) @@ -218,13 +237,7 @@ module.exports = CompileController = { const { project_id } = req.params const { user_id } = req.params const { image } = req.query - if ( - image && - Settings.clsi && - Settings.clsi.docker && - Settings.clsi.docker.allowedImages && - !Settings.clsi.docker.allowedImages.includes(image) - ) { + if (image && !isImageNameAllowed(image)) { return res.status(400).send('invalid image') } logger.log({ image, file, project_id }, 'word count request') diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 25741005..c8010625 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -431,7 +431,15 @@ module.exports = CompileManager = { }) }, // directory exists - syncFromCode(project_id, user_id, file_name, line, column, callback) { + syncFromCode( + project_id, + user_id, + file_name, + line, + column, + imageName, + callback + ) { // If LaTeX was run in a virtual environment, the file path that synctex expects // might not match the file path on the host. The .synctex.gz file however, will be accessed // wherever it is on the host. @@ -444,22 +452,28 @@ module.exports = CompileManager = { const compileDir = getCompileDir(project_id, user_id) const synctex_path = `${base_dir}/output.pdf` const command = ['code', synctex_path, file_path, line, column] - CompileManager._runSynctex(project_id, user_id, command, function ( - error, - stdout - ) { - if (error != null) { - return callback(error) + CompileManager._runSynctex( + project_id, + user_id, + command, + imageName, + function (error, stdout) { + if (error != null) { + return callback(error) + } + logger.log( + { project_id, user_id, file_name, line, column, command, stdout }, + 'synctex code output' + ) + return callback( + null, + CompileManager._parseSynctexFromCodeOutput(stdout) + ) } - logger.log( - { project_id, user_id, file_name, line, column, command, stdout }, - 'synctex code output' - ) - return callback(null, CompileManager._parseSynctexFromCodeOutput(stdout)) - }) + ) }, - syncFromPdf(project_id, user_id, page, h, v, callback) { + syncFromPdf(project_id, user_id, page, h, v, imageName, callback) { if (callback == null) { callback = function (error, filePositions) {} } @@ -468,22 +482,25 @@ module.exports = CompileManager = { const base_dir = Settings.path.synctexBaseDir(compileName) const synctex_path = `${base_dir}/output.pdf` const command = ['pdf', synctex_path, page, h, v] - CompileManager._runSynctex(project_id, user_id, command, function ( - error, - stdout - ) { - if (error != null) { - return callback(error) + CompileManager._runSynctex( + project_id, + user_id, + command, + imageName, + function (error, stdout) { + if (error != null) { + return callback(error) + } + logger.log( + { project_id, user_id, page, h, v, stdout }, + 'synctex pdf output' + ) + return callback( + null, + CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) + ) } - logger.log( - { project_id, user_id, page, h, v, stdout }, - 'synctex pdf output' - ) - return callback( - null, - CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) - ) - }) + ) }, _checkFileExists(dir, filename, callback) { @@ -513,7 +530,7 @@ module.exports = CompileManager = { }) }, - _runSynctex(project_id, user_id, command, callback) { + _runSynctex(project_id, user_id, command, imageName, callback) { if (callback == null) { callback = function (error, stdout) {} } @@ -533,9 +550,10 @@ module.exports = CompileManager = { compileName, command, directory, - Settings.clsi && Settings.clsi.docker - ? Settings.clsi.docker.image - : undefined, + imageName || + (Settings.clsi && Settings.clsi.docker + ? Settings.clsi.docker.image + : undefined), timeout, {}, compileGroup, diff --git a/test/acceptance/js/AllowedImageNames.js b/test/acceptance/js/AllowedImageNames.js index 7e38954e..8107273f 100644 --- a/test/acceptance/js/AllowedImageNames.js +++ b/test/acceptance/js/AllowedImageNames.js @@ -71,6 +71,78 @@ Hello world }) }) + describe('syncToCode', function () { + beforeEach(function (done) { + Client.compile(this.project_id, this.request, done) + }) + it('should error out with an invalid imageName', function () { + Client.syncFromCodeWithImage( + this.project_id, + 'main.tex', + 3, + 5, + 'something/evil:1337', + (error, body) => { + expect(String(error)).to.include('statusCode=400') + expect(body).to.equal('invalid image') + } + ) + }) + + it('should produce a mapping a valid imageName', function () { + Client.syncFromCodeWithImage( + this.project_id, + 'main.tex', + 3, + 5, + process.env.TEXLIVE_IMAGE, + (error, result) => { + expect(error).to.not.exist + expect(result).to.deep.equal({ + pdf: [ + { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } + ] + }) + } + ) + }) + }) + + describe('syncToPdf', function () { + beforeEach(function (done) { + Client.compile(this.project_id, this.request, done) + }) + it('should error out with an invalid imageName', function () { + Client.syncFromPdfWithImage( + this.project_id, + 'main.tex', + 100, + 200, + 'something/evil:1337', + (error, body) => { + expect(String(error)).to.include('statusCode=400') + expect(body).to.equal('invalid image') + } + ) + }) + + it('should produce a mapping a valid imageName', function () { + Client.syncFromPdfWithImage( + this.project_id, + 1, + 100, + 200, + process.env.TEXLIVE_IMAGE, + (error, result) => { + expect(error).to.not.exist + expect(result).to.deep.equal({ + code: [{ file: 'main.tex', line: 3, column: -1 }] + }) + } + ) + }) + }) + describe('wordcount', function () { beforeEach(function (done) { Client.compile(this.project_id, this.request, done) @@ -80,8 +152,9 @@ Hello world this.project_id, 'main.tex', 'something/evil:1337', - (error, result) => { + (error, body) => { expect(String(error)).to.include('statusCode=400') + expect(body).to.equal('invalid image') } ) }) diff --git a/test/acceptance/js/SynctexTests.js b/test/acceptance/js/SynctexTests.js index e636912b..206ada45 100644 --- a/test/acceptance/js/SynctexTests.js +++ b/test/acceptance/js/SynctexTests.js @@ -100,9 +100,7 @@ Hello world 3, 5, (error, body) => { - if (error != null) { - throw error - } + expect(String(error)).to.include('statusCode=404') expect(body).to.equal('Not Found') return done() } @@ -117,9 +115,7 @@ Hello world 100, 200, (error, body) => { - if (error != null) { - throw error - } + expect(String(error)).to.include('statusCode=404') expect(body).to.equal('Not Found') return done() } @@ -160,9 +156,7 @@ Hello world 3, 5, (error, body) => { - if (error != null) { - throw error - } + expect(String(error)).to.include('statusCode=404') expect(body).to.equal('Not Found') return done() } @@ -177,9 +171,7 @@ Hello world 100, 200, (error, body) => { - if (error != null) { - throw error - } + expect(String(error)).to.include('statusCode=404') expect(body).to.equal('Not Found') return done() } diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index 7cd0c114..43825ae4 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -69,6 +69,10 @@ module.exports = Client = { }, syncFromCode(project_id, file, line, column, callback) { + Client.syncFromCodeWithImage(project_id, file, line, column, '', callback) + }, + + syncFromCodeWithImage(project_id, file, line, column, imageName, callback) { if (callback == null) { callback = function (error, pdfPositions) {} } @@ -76,6 +80,7 @@ module.exports = Client = { { url: `${this.host}/project/${project_id}/sync/code`, qs: { + imageName, file, line, column @@ -86,12 +91,19 @@ module.exports = Client = { if (error != null) { return callback(error) } + if (response.statusCode !== 200) { + return callback(new Error(`statusCode=${response.statusCode}`), body) + } return callback(null, body) } ) }, syncFromPdf(project_id, page, h, v, callback) { + Client.syncFromPdfWithImage(project_id, page, h, v, '', callback) + }, + + syncFromPdfWithImage(project_id, page, h, v, imageName, callback) { if (callback == null) { callback = function (error, pdfPositions) {} } @@ -99,6 +111,7 @@ module.exports = Client = { { url: `${this.host}/project/${project_id}/sync/pdf`, qs: { + imageName, page, h, v @@ -109,6 +122,9 @@ module.exports = Client = { if (error != null) { return callback(error) } + if (response.statusCode !== 200) { + return callback(new Error(`statusCode=${response.statusCode}`), body) + } return callback(null, body) } ) @@ -208,7 +224,7 @@ module.exports = Client = { return callback(error) } if (response.statusCode !== 200) { - return callback(new Error(`statusCode=${response.statusCode}`)) + return callback(new Error(`statusCode=${response.statusCode}`), body) } return callback(null, JSON.parse(body)) } diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index d8c7c126..6f256d29 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -18,6 +18,48 @@ const modulePath = require('path').join( ) const tk = require('timekeeper') +function tryImageNameValidation(method, imageNameField) { + describe('when allowedImages is set', function () { + beforeEach(function () { + this.Settings.clsi = { docker: {} } + this.Settings.clsi.docker.allowedImages = [ + 'repo/image:tag1', + 'repo/image:tag2' + ] + this.res.send = sinon.stub() + this.res.status = sinon.stub().returns({ send: this.res.send }) + + this.CompileManager[method].reset() + }) + + describe('with an invalid image', function () { + beforeEach(function () { + this.req.query[imageNameField] = 'something/evil:1337' + this.CompileController[method](this.req, this.res, this.next) + }) + it('should return a 400', function () { + expect(this.res.status.calledWith(400)).to.equal(true) + }) + it('should not run the query', function () { + expect(this.CompileManager[method].called).to.equal(false) + }) + }) + + describe('with a valid image', function () { + beforeEach(function () { + this.req.query[imageNameField] = 'repo/image:tag1' + this.CompileController[method](this.req, this.res, this.next) + }) + it('should not return a 400', function () { + expect(this.res.status.calledWith(400)).to.equal(false) + }) + it('should run the query', function () { + expect(this.CompileManager[method].called).to.equal(true) + }) + }) + }) +} + describe('CompileController', function () { beforeEach(function () { this.CompileController = SandboxedModule.require(modulePath, { @@ -248,7 +290,7 @@ describe('CompileController', function () { this.CompileManager.syncFromCode = sinon .stub() - .callsArgWith(5, null, (this.pdfPositions = ['mock-positions'])) + .yields(null, (this.pdfPositions = ['mock-positions'])) return this.CompileController.syncFromCode(this.req, this.res, this.next) }) @@ -264,13 +306,15 @@ describe('CompileController', function () { .should.equal(true) }) - return it('should return the positions', function () { + it('should return the positions', function () { return this.res.json .calledWith({ pdf: this.pdfPositions }) .should.equal(true) }) + + tryImageNameValidation('syncFromCode', 'imageName') }) describe('syncFromPdf', function () { @@ -289,7 +333,7 @@ describe('CompileController', function () { this.CompileManager.syncFromPdf = sinon .stub() - .callsArgWith(5, null, (this.codePositions = ['mock-positions'])) + .yields(null, (this.codePositions = ['mock-positions'])) return this.CompileController.syncFromPdf(this.req, this.res, this.next) }) @@ -299,13 +343,15 @@ describe('CompileController', function () { .should.equal(true) }) - return it('should return the positions', function () { + it('should return the positions', function () { return this.res.json .calledWith({ code: this.codePositions }) .should.equal(true) }) + + tryImageNameValidation('syncFromPdf', 'imageName') }) return describe('wordcount', function () { @@ -340,42 +386,6 @@ describe('CompileController', function () { .should.equal(true) }) - describe('when allowedImages is set', function () { - beforeEach(function () { - this.Settings.clsi = { docker: {} } - this.Settings.clsi.docker.allowedImages = [ - 'repo/image:tag1', - 'repo/image:tag2' - ] - this.res.send = sinon.stub() - this.res.status = sinon.stub().returns({ send: this.res.send }) - }) - - describe('with an invalid image', function () { - beforeEach(function () { - this.req.query.image = 'something/evil:1337' - this.CompileController.wordcount(this.req, this.res, this.next) - }) - it('should return a 400', function () { - expect(this.res.status.calledWith(400)).to.equal(true) - }) - it('should not run the query', function () { - expect(this.CompileManager.wordcount.called).to.equal(false) - }) - }) - - describe('with a valid image', function () { - beforeEach(function () { - this.req.query.image = 'repo/image:tag1' - this.CompileController.wordcount(this.req, this.res, this.next) - }) - it('should not return a 400', function () { - expect(this.res.status.calledWith(400)).to.equal(false) - }) - it('should run the query', function () { - expect(this.CompileManager.wordcount.called).to.equal(true) - }) - }) - }) + tryImageNameValidation('wordcount', 'image') }) }) diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index e33783a1..2d40bdd6 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -394,13 +394,14 @@ describe('CompileManager', function () { this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n` this.CommandRunner.run = sinon .stub() - .callsArgWith(7, null, { stdout: this.stdout }) + .yields(null, { stdout: this.stdout }) return this.CompileManager.syncFromCode( this.project_id, this.user_id, this.file_name, this.line, this.column, + '', this.callback ) }) @@ -428,7 +429,7 @@ describe('CompileManager', function () { .should.equal(true) }) - return it('should call the callback with the parsed output', function () { + it('should call the callback with the parsed output', function () { return this.callback .calledWith(null, [ { @@ -441,6 +442,44 @@ describe('CompileManager', function () { ]) .should.equal(true) }) + + describe('with a custom imageName', function () { + const customImageName = 'foo/bar:tag-0' + beforeEach(function () { + this.CommandRunner.run.reset() + this.CompileManager.syncFromCode( + this.project_id, + this.user_id, + this.file_name, + this.line, + this.column, + customImageName, + this.callback + ) + }) + + it('should execute the synctex binary in a custom docker image', function () { + const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf` + const file_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}` + this.CommandRunner.run + .calledWith( + `${this.project_id}-${this.user_id}`, + [ + '/opt/synctex', + 'code', + synctex_path, + file_path, + this.line, + this.column + ], + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + customImageName, + 60000, + {} + ) + .should.equal(true) + }) + }) }) return describe('syncFromPdf', function () { @@ -460,6 +499,7 @@ describe('CompileManager', function () { this.page, this.h, this.v, + '', this.callback ) }) @@ -479,7 +519,7 @@ describe('CompileManager', function () { .should.equal(true) }) - return it('should call the callback with the parsed output', function () { + it('should call the callback with the parsed output', function () { return this.callback .calledWith(null, [ { @@ -490,6 +530,36 @@ describe('CompileManager', function () { ]) .should.equal(true) }) + + describe('with a custom imageName', function () { + const customImageName = 'foo/bar:tag-1' + beforeEach(function () { + this.CommandRunner.run.reset() + this.CompileManager.syncFromPdf( + this.project_id, + this.user_id, + this.page, + this.h, + this.v, + customImageName, + this.callback + ) + }) + + it('should execute the synctex binary in a custom docker image', function () { + const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf` + this.CommandRunner.run + .calledWith( + `${this.project_id}-${this.user_id}`, + ['/opt/synctex', 'pdf', synctex_path, this.page, this.h, this.v], + `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, + customImageName, + 60000, + {} + ) + .should.equal(true) + }) + }) }) }) From eb9aad19d912ffe0b77e1448c803749ca1c95938 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Mar 2021 17:52:26 +0000 Subject: [PATCH 647/709] Bump y18n from 4.0.0 to 4.0.1 Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1. - [Release notes](https://github.com/yargs/y18n/releases) - [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md) - [Commits](https://github.com/yargs/y18n/commits) Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4fb21c0..04e7b8dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7418,9 +7418,9 @@ } }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { From 20efce37cb33e475139ebd6e4f504af06f0ffe42 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Thu, 29 Apr 2021 15:30:54 +0100 Subject: [PATCH 648/709] [misc] add linting for missing explicit dependencies and fix any errors --- .eslintrc | 13 +++++++++++-- app/js/LocalCommandRunner.js | 2 +- buildscript.txt | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.eslintrc b/.eslintrc index 76dad156..321353f9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,7 +22,10 @@ "rules": { // Swap the no-unused-expressions rule with a more chai-friendly one "no-unused-expressions": 0, - "chai-friendly/no-unused-expressions": "error" + "chai-friendly/no-unused-expressions": "error", + + // Do not allow importing of implicit dependencies. + "import/no-extraneous-dependencies": "error" }, "overrides": [ { @@ -57,7 +60,13 @@ "files": ["app/**/*.js", "app.js", "index.js"], "rules": { // don't allow console.log in backend code - "no-console": "error" + "no-console": "error", + + // Do not allow importing of implicit dependencies. + "import/no-extraneous-dependencies": ["error", { + // Do not allow importing of devDependencies. + "devDependencies": false + }] } } ] diff --git a/app/js/LocalCommandRunner.js b/app/js/LocalCommandRunner.js index 6f57731c..d5fd3090 100644 --- a/app/js/LocalCommandRunner.js +++ b/app/js/LocalCommandRunner.js @@ -15,7 +15,7 @@ */ let CommandRunner const { spawn } = require('child_process') -const _ = require('underscore') +const _ = require('lodash') const logger = require('logger-sharelatex') logger.info('using standard command runner') diff --git a/buildscript.txt b/buildscript.txt index a86911dc..84a43ecb 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -6,4 +6,4 @@ clsi --env-pass-through=TEXLIVE_IMAGE --node-version=12.21.0 --public-repo=True ---script-version=3.7.0 +--script-version=3.8.0 From 47a4589e1690eafdf3162c5a38df153549895439 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 May 2021 14:32:37 +0000 Subject: [PATCH 649/709] Bump hosted-git-info from 2.8.5 to 2.8.9 Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.5 to 2.8.9. - [Release notes](https://github.com/npm/hosted-git-info/releases) - [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md) - [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.5...v2.8.9) Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4fb21c0..2f94be10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3536,9 +3536,9 @@ "integrity": "sha512-Yu+q/XWr2fFQ11tHxPq4p4EiNkb2y+lAacJNhAdRXVfRIcDH6gi7htWFnnlIzvqHMHoWeIsfXlNAjZInpAOJDA==" }, "hosted-git-info": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", - "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "http-errors": { From 8ba4397caa2f8ec4f88fc70c8af3f641d23ad877 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Thu, 13 May 2021 15:07:54 +0200 Subject: [PATCH 650/709] [misc] merge pdf caching into main (#226) * wip generate directory for hash content * cleanup, remove console logging * add content caching module * Return PDF stream ranges with compile response * Return the PDF file size in the compile response * PDF range endpoint * [misc] WIP: pdf caching: preserve the m-time on static content files * [misc] WIP: pdf caching: improve browser caching, emit caching headers * [misc] WIP: pdf caching: do not emit very small chunks <1kB * [misc] keep up with moving output files into a separate directory * [OutputCacheManager] add global feature flag for enabling pdf caching * [misc] add contentId into the URL for protecting PDF stream contents * [misc] support PDF stream caching for anonymous users * [misc] add per-request feature flag for enabling PDF stream caching * [misc] enable pdf caching in CI and emit metrics at the end of run * [misc] expose compile stats and timings to the frontend * [misc] log an error in case saving output files fails * [misc] add metrics for pdf bandwidth and pdf caching performance * [misc] add a dark mode to the pdf caching for computing ranges only * [misc] move pdf caching metrics into ContentCacheMetrics * [misc] add a config option for the min chunk size of pdf ranges Co-authored-by: Brian Gough <brian.gough@overleaf.com> Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com> --- app.js | 10 ++ app/js/CompileController.js | 40 ++++--- app/js/CompileManager.js | 28 ++++- app/js/ContentCacheManager.js | 118 +++++++++++++++++++++ app/js/ContentCacheMetrics.js | 80 ++++++++++++++ app/js/ContentController.js | 24 +++++ app/js/OutputCacheManager.js | 138 ++++++++++++++++++++++++- app/js/RequestParser.js | 8 ++ config/settings.defaults.js | 7 +- docker-compose.ci.yml | 1 + docker-compose.yml | 1 + test/acceptance/js/Stats.js | 16 +++ test/acceptance/js/helpers/Client.js | 4 + test/unit/js/CompileControllerTests.js | 33 ++++-- test/unit/js/CompileManagerTests.js | 6 +- 15 files changed, 487 insertions(+), 27 deletions(-) create mode 100644 app/js/ContentCacheManager.js create mode 100644 app/js/ContentCacheMetrics.js create mode 100644 app/js/ContentController.js create mode 100644 test/acceptance/js/Stats.js diff --git a/app.js b/app.js index f24ce5f9..6266c86b 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,7 @@ const Metrics = require('@overleaf/metrics') Metrics.initialize('clsi') const CompileController = require('./app/js/CompileController') +const ContentController = require('./app/js/ContentController') const Settings = require('settings-sharelatex') const logger = require('logger-sharelatex') logger.initialize('clsi') @@ -170,6 +171,15 @@ app.get( } ) +app.get( + '/project/:projectId/content/:contentId/:hash', + ContentController.getPdfRange +) +app.get( + '/project/:projectId/user/:userId/content/:contentId/:hash', + ContentController.getPdfRange +) + app.get('/project/:project_id/build/:build_id/output/*', function ( req, res, diff --git a/app/js/CompileController.js b/app/js/CompileController.js index ddf83d7c..2813bca9 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -49,7 +49,9 @@ module.exports = CompileController = { } return CompileManager.doCompileWithLock(request, function ( error, - outputFiles + outputFiles, + stats, + timings ) { let code, status if (outputFiles == null) { @@ -118,18 +120,30 @@ module.exports = CompileController = { compile: { status, error: (error != null ? error.message : undefined) || error, - outputFiles: outputFiles.map((file) => ({ - url: - `${Settings.apis.clsi.url}/project/${request.project_id}` + - (request.user_id != null - ? `/user/${request.user_id}` - : '') + - (file.build != null ? `/build/${file.build}` : '') + - `/output/${file.path}`, - path: file.path, - type: file.type, - build: file.build - })) + stats, + timings, + outputFiles: outputFiles.map((file) => { + const record = { + url: + `${Settings.apis.clsi.url}/project/${request.project_id}` + + (request.user_id != null + ? `/user/${request.user_id}` + : '') + + (file.build != null ? `/build/${file.build}` : '') + + `/output/${file.path}`, + path: file.path, + type: file.type, + build: file.build, + contentId: file.contentId + } + if (file.ranges != null) { + record.ranges = file.ranges + } + if (file.size != null) { + record.size = file.size + } + return record + }) } }) }) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index c8010625..c771082b 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -34,6 +34,7 @@ const os = require('os') const async = require('async') const Errors = require('./Errors') const CommandRunner = require('./CommandRunner') +const { emitPdfStats } = require('./ContentCacheMetrics') const getCompileName = function (project_id, user_id) { if (user_id != null) { @@ -77,6 +78,7 @@ module.exports = CompileManager = { const compileDir = getCompileDir(request.project_id, request.user_id) const outputDir = getOutputDir(request.project_id, request.user_id) + const timerE2E = new Metrics.Timer('compile-e2e') let timer = new Metrics.Timer('write-to-disk') logger.log( { project_id: request.project_id, user_id: request.user_id }, @@ -249,11 +251,13 @@ module.exports = CompileManager = { return callback(error) } Metrics.inc('compiles-succeeded') + stats = stats || {} const object = stats || {} for (metric_key in object) { metric_value = object[metric_key] Metrics.count(metric_key, metric_value) } + timings = timings || {} const object1 = timings || {} for (metric_key in object1) { metric_value = object1[metric_key] @@ -297,10 +301,32 @@ module.exports = CompileManager = { return callback(error) } return OutputCacheManager.saveOutputFiles( + { request, stats, timings }, outputFiles, compileDir, outputDir, - (error, newOutputFiles) => callback(null, newOutputFiles) + (err, newOutputFiles) => { + if (err) { + const { + project_id: projectId, + user_id: userId + } = request + logger.err( + { projectId, userId, err }, + 'failed to save output files' + ) + } + + // Emit compile time. + timings.compile = ts + timings.compileE2E = timerE2E.done() + + if (stats['pdf-size']) { + emitPdfStats(stats, timings) + } + + callback(null, newOutputFiles, stats, timings) + } ) } ) diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js new file mode 100644 index 00000000..94ed9ebc --- /dev/null +++ b/app/js/ContentCacheManager.js @@ -0,0 +1,118 @@ +/** + * ContentCacheManager - maintains a cache of stream hashes from a PDF file + */ + +const { callbackify } = require('util') +const fs = require('fs') +const crypto = require('crypto') +const Path = require('path') +const Settings = require('settings-sharelatex') + +const MIN_CHUNK_SIZE = Settings.pdfCachingMinChunkSize + +/** + * + * @param {String} contentDir path to directory where content hash files are cached + * @param {String} filePath the pdf file to scan for streams + */ +async function update(contentDir, filePath) { + const stream = fs.createReadStream(filePath) + const extractor = new PdfStreamsExtractor() + const ranges = [] + const newRanges = [] + for await (const chunk of stream) { + const pdfStreams = extractor.consume(chunk) + for (const pdfStream of pdfStreams) { + if (pdfStream.end - pdfStream.start < MIN_CHUNK_SIZE) continue + const hash = pdfStreamHash(pdfStream.buffers) + const range = { start: pdfStream.start, end: pdfStream.end, hash } + ranges.push(range) + if (await writePdfStream(contentDir, hash, pdfStream.buffers)) { + newRanges.push(range) + } + } + } + return [ranges, newRanges] +} + +class PdfStreamsExtractor { + constructor() { + this.fileIndex = 0 + this.inStream = false + this.streamStartIndex = 0 + this.buffers = [] + } + + consume(chunk) { + let chunkIndex = 0 + const pdfStreams = [] + while (true) { + if (!this.inStream) { + // Not in a stream, look for stream start + const index = chunk.indexOf('stream', chunkIndex) + if (index === -1) { + // Couldn't find stream start + break + } + // Found stream start, start a stream + this.inStream = true + this.streamStartIndex = this.fileIndex + index + chunkIndex = index + } else { + // In a stream, look for stream end + const index = chunk.indexOf('endstream', chunkIndex) + if (index === -1) { + this.buffers.push(chunk.slice(chunkIndex)) + break + } + // add "endstream" part + const endIndex = index + 9 + this.buffers.push(chunk.slice(chunkIndex, endIndex)) + pdfStreams.push({ + start: this.streamStartIndex, + end: this.fileIndex + endIndex, + buffers: this.buffers + }) + this.inStream = false + this.buffers = [] + chunkIndex = endIndex + } + } + this.fileIndex += chunk.length + return pdfStreams + } +} + +function pdfStreamHash(buffers) { + const hash = crypto.createHash('sha256') + for (const buffer of buffers) { + hash.update(buffer) + } + return hash.digest('hex') +} + +async function writePdfStream(dir, hash, buffers) { + const filename = Path.join(dir, hash) + try { + await fs.promises.stat(filename) + // The file exists. Do not rewrite the content. + // It would change the modified-time of the file and hence invalidate the + // ETags used for client side caching via browser internals. + return false + } catch (e) {} + const file = await fs.promises.open(filename, 'w') + if (Settings.enablePdfCachingDark) { + // Write an empty file in dark mode. + buffers = [] + } + try { + for (const buffer of buffers) { + await file.write(buffer) + } + } finally { + await file.close() + } + return true +} + +module.exports = { update: callbackify(update) } diff --git a/app/js/ContentCacheMetrics.js b/app/js/ContentCacheMetrics.js new file mode 100644 index 00000000..04b475f8 --- /dev/null +++ b/app/js/ContentCacheMetrics.js @@ -0,0 +1,80 @@ +const Metrics = require('./Metrics') + +const ONE_MB = 1024 * 1024 + +function emitPdfStats(stats, timings) { + if (timings['compute-pdf-caching']) { + emitPdfCachingStats(stats, timings) + } else { + // How much bandwidth will the pdf incur when downloaded in full? + Metrics.summary('pdf-bandwidth', stats['pdf-size']) + } +} + +function emitPdfCachingStats(stats, timings) { + if (!stats['pdf-size']) return // double check + + // How large is the overhead of hashing up-front? + const fraction = + timings.compileE2E - timings['compute-pdf-caching'] !== 0 + ? timings.compileE2E / + (timings.compileE2E - timings['compute-pdf-caching']) + : 1 + Metrics.summary('overhead-compute-pdf-ranges', fraction * 100 - 100) + + // How does the hashing scale to pdf size in MB? + Metrics.timing( + 'compute-pdf-caching-relative-to-pdf-size', + timings['compute-pdf-caching'] / (stats['pdf-size'] / ONE_MB) + ) + if (stats['pdf-caching-total-ranges-size']) { + // How does the hashing scale to total ranges size in MB? + Metrics.timing( + 'compute-pdf-caching-relative-to-total-ranges-size', + timings['compute-pdf-caching'] / + (stats['pdf-caching-total-ranges-size'] / ONE_MB) + ) + // How fast is the hashing per range on average? + Metrics.timing( + 'compute-pdf-caching-relative-to-ranges-count', + timings['compute-pdf-caching'] / stats['pdf-caching-n-ranges'] + ) + + // How many ranges are new? + Metrics.summary( + 'new-pdf-ranges-relative-to-total-ranges', + (stats['pdf-caching-n-new-ranges'] / stats['pdf-caching-n-ranges']) * 100 + ) + } + + // How much content is cacheable? + Metrics.summary( + 'cacheable-ranges-to-pdf-size', + (stats['pdf-caching-total-ranges-size'] / stats['pdf-size']) * 100 + ) + + const sizeWhenDownloadedInFull = + // All of the pdf + stats['pdf-size'] - + // These ranges are potentially cached. + stats['pdf-caching-total-ranges-size'] + + // These ranges are not cached. + stats['pdf-caching-new-ranges-size'] + + // How much bandwidth can we save when downloading the pdf in full? + Metrics.summary( + 'pdf-bandwidth-savings', + 100 - (sizeWhenDownloadedInFull / stats['pdf-size']) * 100 + ) + + // How much bandwidth will the pdf incur when downloaded in full? + Metrics.summary('pdf-bandwidth', sizeWhenDownloadedInFull) + + // How much space do the ranges use? + // This will accumulate the ranges size over time, skipping already written ranges. + Metrics.summary('pdf-ranges-disk-size', stats['pdf-caching-new-ranges-size']) +} + +module.exports = { + emitPdfStats +} diff --git a/app/js/ContentController.js b/app/js/ContentController.js new file mode 100644 index 00000000..76478def --- /dev/null +++ b/app/js/ContentController.js @@ -0,0 +1,24 @@ +const Path = require('path') +const send = require('send') +const Settings = require('settings-sharelatex') +const OutputCacheManager = require('./OutputCacheManager') + +const ONE_DAY_S = 24 * 60 * 60 +const ONE_DAY_MS = ONE_DAY_S * 1000 + +function getPdfRange(req, res, next) { + const { projectId, userId, contentId, hash } = req.params + const perUserDir = userId ? `${projectId}-${userId}` : projectId + const path = Path.join( + Settings.path.outputDir, + perUserDir, + OutputCacheManager.CONTENT_SUBDIR, + contentId, + hash + ) + res.setHeader('cache-control', `public, max-age=${ONE_DAY_S}`) + res.setHeader('expires', new Date(Date.now() + ONE_DAY_MS).toUTCString()) + send(req, path).pipe(res) +} + +module.exports = { getPdfRange } diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index 8aefb9b7..cd1bd883 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -22,10 +22,13 @@ const logger = require('logger-sharelatex') const _ = require('lodash') const Settings = require('settings-sharelatex') const crypto = require('crypto') +const Metrics = require('./Metrics') const OutputFileOptimiser = require('./OutputFileOptimiser') +const ContentCacheManager = require('./ContentCacheManager') module.exports = OutputCacheManager = { + CONTENT_SUBDIR: 'content', CACHE_SUBDIR: 'generated-files', ARCHIVE_SUBDIR: 'archived-logs', // build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes @@ -59,7 +62,13 @@ module.exports = OutputCacheManager = { }) }, - saveOutputFiles(outputFiles, compileDir, outputDir, callback) { + saveOutputFiles( + { request, stats, timings }, + outputFiles, + compileDir, + outputDir, + callback + ) { if (callback == null) { callback = function (error) {} } @@ -72,7 +81,31 @@ module.exports = OutputCacheManager = { compileDir, outputDir, buildId, - callback + function (err, result) { + if (err != null) { + return callback(err) + } + OutputCacheManager.collectOutputPdfSize( + result, + outputDir, + stats, + (err, result) => { + if (err) return callback(err, result) + + if (!Settings.enablePdfCaching || !request.enablePdfCaching) { + return callback(null, result) + } + + OutputCacheManager.saveStreamsInContentDir( + { stats, timings }, + result, + compileDir, + outputDir, + callback + ) + } + ) + } ) }) }, @@ -206,6 +239,107 @@ module.exports = OutputCacheManager = { }) }, + collectOutputPdfSize(outputFiles, outputDir, stats, callback) { + const outputFile = outputFiles.find((x) => x.path === 'output.pdf') + if (!outputFile) return callback(null, outputFiles) + const outputFilePath = Path.join( + outputDir, + OutputCacheManager.path(outputFile.build, outputFile.path) + ) + fs.stat(outputFilePath, (err, stat) => { + if (err) return callback(err, outputFiles) + + outputFile.size = stat.size + stats['pdf-size'] = outputFile.size + callback(null, outputFiles) + }) + }, + + saveStreamsInContentDir( + { stats, timings }, + outputFiles, + compileDir, + outputDir, + callback + ) { + const cacheRoot = Path.join(outputDir, OutputCacheManager.CONTENT_SUBDIR) + // check if content dir exists + OutputCacheManager.ensureContentDir(cacheRoot, function (err, contentDir) { + if (err) return callback(err, outputFiles) + + const outputFile = outputFiles.find((x) => x.path === 'output.pdf') + if (outputFile) { + // possibly we should copy the file from the build dir here + const outputFilePath = Path.join( + outputDir, + OutputCacheManager.path(outputFile.build, outputFile.path) + ) + const timer = new Metrics.Timer('compute-pdf-ranges') + ContentCacheManager.update(contentDir, outputFilePath, function ( + err, + ranges + ) { + if (err) return callback(err, outputFiles) + const [contentRanges, newContentRanges] = ranges + + if (Settings.enablePdfCachingDark) { + // In dark mode we are doing the computation only and do not emit + // any ranges to the frontend. + } else { + outputFile.contentId = Path.basename(contentDir) + outputFile.ranges = contentRanges + } + + timings['compute-pdf-caching'] = timer.done() + stats['pdf-caching-n-ranges'] = contentRanges.length + stats['pdf-caching-total-ranges-size'] = contentRanges.reduce( + (sum, next) => sum + (next.end - next.start), + 0 + ) + stats['pdf-caching-n-new-ranges'] = newContentRanges.length + stats['pdf-caching-new-ranges-size'] = newContentRanges.reduce( + (sum, next) => sum + (next.end - next.start), + 0 + ) + callback(null, outputFiles) + }) + } else { + callback(null, outputFiles) + } + }) + }, + + ensureContentDir(contentRoot, callback) { + fse.ensureDir(contentRoot, function (err) { + if (err != null) { + return callback(err) + } + fs.readdir(contentRoot, function (err, results) { + const dirs = results.sort() + const contentId = dirs.find((dir) => + OutputCacheManager.BUILD_REGEX.test(dir) + ) + if (contentId) { + callback(null, Path.join(contentRoot, contentId)) + } else { + // make a content directory + OutputCacheManager.generateBuildId(function (err, contentId) { + if (err) { + return callback(err) + } + const contentDir = Path.join(contentRoot, contentId) + fse.ensureDir(contentDir, function (err) { + if (err) { + return callback(err) + } + return callback(null, contentDir) + }) + }) + } + }) + }) + }, + archiveLogs(outputFiles, compileDir, outputDir, buildId, callback) { if (callback == null) { callback = function (error) {} diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index 43772044..66c917ae 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -50,6 +50,14 @@ module.exports = RequestParser = { type: 'string' } ) + response.enablePdfCaching = this._parseAttribute( + 'enablePdfCaching', + compile.options.enablePdfCaching, + { + default: false, + type: 'boolean' + } + ) response.timeout = this._parseAttribute( 'timeout', compile.options.timeout, diff --git a/config/settings.defaults.js b/config/settings.defaults.js index a0ad8433..b65a0193 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -63,7 +63,12 @@ module.exports = { texliveOpenoutAny: process.env.TEXLIVE_OPENOUT_ANY, sentry: { dsn: process.env.SENTRY_DSN - } + }, + + enablePdfCaching: process.env.ENABLE_PDF_CACHING === 'true', + enablePdfCachingDark: process.env.ENABLE_PDF_CACHING_DARK === 'true', + pdfCachingMinChunkSize: + parseInt(process.env.PDF_CACHING_MIN_CHUNK_SIZE, 10) || 1024 } if (process.env.ALLOWED_COMPILE_GROUPS) { diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 22379337..245059ad 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -29,6 +29,7 @@ services: NODE_ENV: test NODE_OPTIONS: "--unhandled-rejections=strict" TEXLIVE_IMAGE: + ENABLE_PDF_CACHING: "true" command: npm run test:acceptance:_run diff --git a/docker-compose.yml b/docker-compose.yml index 66880002..ee68a7c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,5 +38,6 @@ services: LOG_LEVEL: ERROR NODE_ENV: test NODE_OPTIONS: "--unhandled-rejections=strict" + ENABLE_PDF_CACHING: "true" command: npm run --silent test:acceptance diff --git a/test/acceptance/js/Stats.js b/test/acceptance/js/Stats.js new file mode 100644 index 00000000..87b20b1c --- /dev/null +++ b/test/acceptance/js/Stats.js @@ -0,0 +1,16 @@ +const request = require('request') +const Settings = require('settings-sharelatex') +after(function (done) { + request( + { + url: `${Settings.apis.clsi.url}/metrics` + }, + (err, response, body) => { + if (err) return done(err) + console.error('-- metrics --') + console.error(body) + console.error('-- metrics --') + done() + } + ) +}) diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index 43825ae4..c5814ff3 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -30,6 +30,10 @@ module.exports = Client = { if (callback == null) { callback = function (error, res, body) {} } + if (data) { + // Enable pdf caching unless disabled explicitly. + data.options = Object.assign({}, { enablePdfCaching: true }, data.options) + } return request.post( { url: `${this.host}/project/${project_id}/compile`, diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 6f256d29..6d6b34ee 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -113,6 +113,8 @@ describe('CompileController', function () { this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon .stub() .callsArg(1) + this.stats = { foo: 1 } + this.timings = { bar: 2 } this.res.status = sinon.stub().returnsThis() return (this.res.send = sinon.stub()) }) @@ -121,7 +123,7 @@ describe('CompileController', function () { beforeEach(function () { this.CompileManager.doCompileWithLock = sinon .stub() - .callsArgWith(1, null, this.output_files) + .yields(null, this.output_files, this.stats, this.timings) return this.CompileController.compile(this.req, this.res) }) @@ -150,12 +152,16 @@ describe('CompileController', function () { compile: { status: 'success', error: null, + stats: this.stats, + timings: this.timings, outputFiles: this.output_files.map((file) => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, path: file.path, type: file.type, - build: file.build + build: file.build, + // gets dropped by JSON.stringify + contentId: undefined } }) } @@ -180,7 +186,7 @@ describe('CompileController', function () { ] this.CompileManager.doCompileWithLock = sinon .stub() - .callsArgWith(1, null, this.output_files) + .yields(null, this.output_files, this.stats, this.timings) this.CompileController.compile(this.req, this.res) }) @@ -191,12 +197,16 @@ describe('CompileController', function () { compile: { status: 'failure', error: null, + stats: this.stats, + timings: this.timings, outputFiles: this.output_files.map((file) => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, path: file.path, type: file.type, - build: file.build + build: file.build, + // gets dropped by JSON.stringify + contentId: undefined } }) } @@ -220,7 +230,10 @@ describe('CompileController', function () { compile: { status: 'error', error: this.message, - outputFiles: [] + outputFiles: [], + // JSON.stringify will omit these + stats: undefined, + timings: undefined } }) .should.equal(true) @@ -244,7 +257,10 @@ describe('CompileController', function () { compile: { status: 'timedout', error: this.message, - outputFiles: [] + outputFiles: [], + // JSON.stringify will omit these + stats: undefined, + timings: undefined } }) .should.equal(true) @@ -266,7 +282,10 @@ describe('CompileController', function () { compile: { error: null, status: 'failure', - outputFiles: [] + outputFiles: [], + // JSON.stringify will omit these + stats: undefined, + timings: undefined } }) .should.equal(true) diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index 2d40bdd6..97e318a4 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -71,7 +71,7 @@ describe('CompileManager', function () { this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}` this.CompileManager.doCompile = sinon .stub() - .callsArgWith(1, null, this.output_files) + .yields(null, this.output_files) return (this.LockManager.runWithLock = (lockFile, runner, callback) => runner((err, ...result) => callback(err, ...Array.from(result)))) }) @@ -172,10 +172,10 @@ describe('CompileManager', function () { this.LatexRunner.runLatex = sinon.stub().callsArg(2) this.OutputFileFinder.findOutputFiles = sinon .stub() - .callsArgWith(2, null, this.output_files) + .yields(null, this.output_files) this.OutputCacheManager.saveOutputFiles = sinon .stub() - .callsArgWith(3, null, this.build_files) + .yields(null, this.build_files) this.DraftModeManager.injectDraftMode = sinon.stub().callsArg(1) return (this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false)) }) From 9bd6dc20e8fdc92ca60c9c1e26888997d1937642 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 12 May 2021 09:47:35 +0100 Subject: [PATCH 651/709] log the expiry timeout value when disk is low --- app/js/ProjectPersistenceManager.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index 536630a8..d0fd4c24 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -38,7 +38,10 @@ module.exports = ProjectPersistenceManager = { const lowerExpiry = ProjectPersistenceManager.EXPIRY_TIMEOUT * 0.9 if (lowDisk && Settings.project_cache_length_ms / 2 < lowerExpiry) { logger.warn( - { stats: stats }, + { + stats: stats, + newExpiryTimeoutInDays: (lowerExpiry / oneDay).toFixed(2) + }, 'disk running low on space, modifying EXPIRY_TIMEOUT' ) ProjectPersistenceManager.EXPIRY_TIMEOUT = lowerExpiry From e2b869707ee1ddb6a8611013165629cc91ae66d5 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Thu, 13 May 2021 14:56:15 +0100 Subject: [PATCH 652/709] add validation for express :content_id parameter --- app.js | 21 +++++++++++++++++++++ app/js/ContentCacheManager.js | 5 ++++- app/js/OutputCacheManager.js | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 6266c86b..77ab9229 100644 --- a/app.js +++ b/app.js @@ -29,6 +29,7 @@ Metrics.memory.monitor(logger) const ProjectPersistenceManager = require('./app/js/ProjectPersistenceManager') const OutputCacheManager = require('./app/js/OutputCacheManager') +const ContentCacheManager = require('./app/js/ContentCacheManager') require('./app/js/db').sync() @@ -76,6 +77,26 @@ app.param('build_id', function (req, res, next, buildId) { } }) +app.param('contentId', function (req, res, next, contentId) { + if ( + contentId != null + ? contentId.match(OutputCacheManager.CONTENT_REGEX) + : undefined + ) { + return next() + } else { + return next(new Error(`invalid content id ${contentId}`)) + } +}) + +app.param('hash', function (req, res, next, hash) { + if (hash != null ? hash.match(ContentCacheManager.HASH_REGEX) : undefined) { + return next() + } else { + return next(new Error(`invalid hash ${hash}`)) + } +}) + app.post( '/project/:project_id/compile', bodyParser.json({ limit: Settings.compileSizeLimit }), diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 94ed9ebc..099f0ee8 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -115,4 +115,7 @@ async function writePdfStream(dir, hash, buffers) { return true } -module.exports = { update: callbackify(update) } +module.exports = { + HASH_REGEX: /^[0-9a-f]{64}$/, + update: callbackify(update) +} diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index cd1bd883..fe23ae3e 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -34,6 +34,7 @@ module.exports = OutputCacheManager = { // build id is HEXDATE-HEXRANDOM from Date.now()and RandomBytes // for backwards compatibility, make the randombytes part optional BUILD_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/, + CONTENT_REGEX: /^[0-9a-f]+(-[0-9a-f]+)?$/, CACHE_LIMIT: 2, // maximum number of cache directories CACHE_AGE: 60 * 60 * 1000, // up to one hour old From 7c2f99c890fc79d0257ae51e32fd8627b2147e70 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 14 May 2021 15:49:20 +0100 Subject: [PATCH 653/709] use fse.copy for performance it uses the native fs.copyFile method --- app/js/UrlCache.js | 26 ++++++-------------------- test/unit/js/UrlCacheTests.js | 5 +++-- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index e8ee10dc..90c6486f 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -19,6 +19,7 @@ const UrlFetcher = require('./UrlFetcher') const Settings = require('settings-sharelatex') const crypto = require('crypto') const fs = require('fs') +const fse = require('fs-extra') const logger = require('logger-sharelatex') const async = require('async') @@ -35,8 +36,12 @@ module.exports = UrlCache = { if (error != null) { return callback(error) } - return UrlCache._copyFile(pathToCachedUrl, destPath, function (error) { + return fse.copy(pathToCachedUrl, destPath, function (error) { if (error != null) { + logger.error( + { err: error, from: pathToCachedUrl, to: destPath }, + 'error copying file from cache' + ) return UrlCache._clearUrlDetails(project_id, url, () => callback(error) ) @@ -163,25 +168,6 @@ module.exports = UrlCache = { )}` }, - _copyFile(from, to, _callback) { - if (_callback == null) { - _callback = function (error) {} - } - const callbackOnce = function (error) { - if (error != null) { - logger.error({ err: error, from, to }, 'error copying file from cache') - } - _callback(error) - return (_callback = function () {}) - } - const writeStream = fs.createWriteStream(to) - const readStream = fs.createReadStream(from) - writeStream.on('error', callbackOnce) - readStream.on('error', callbackOnce) - writeStream.on('close', callbackOnce) - return writeStream.on('open', () => readStream.pipe(writeStream)) - }, - _clearUrlFromCache(project_id, url, callback) { if (callback == null) { callback = function (error) {} diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index 2b991245..7276b132 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -27,7 +27,8 @@ describe('UrlCache', function () { 'settings-sharelatex': (this.Settings = { path: { clsiCacheDir: '/cache/dir' } }), - fs: (this.fs = {}) + fs: (this.fs = {}), + 'fs-extra': (this.fse = { copy: sinon.stub().yields() }) } })) }) @@ -268,7 +269,7 @@ describe('UrlCache', function () { }) it('should copy the file to the new location', function () { - return this.UrlCache._copyFile + return this.fse.copy .calledWith(this.cachePath, this.destPath) .should.equal(true) }) From de365882500f60bd8625ac3dbf136540c684452e Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Fri, 14 May 2021 15:58:11 +0100 Subject: [PATCH 654/709] upgrade fs-extra --- package-lock.json | 31 ++++++++++++++++--------------- package.json | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4fb21c0..4a662040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3075,13 +3075,13 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", "requires": { "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" } }, "fs-minipass": { @@ -3419,9 +3419,9 @@ } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "gtoken": { "version": "4.1.4", @@ -4032,11 +4032,12 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" } }, "jsprim": { @@ -7102,9 +7103,9 @@ "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" }, "unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index eb194d9e..3afa29c4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "diskusage": "^1.1.3", "dockerode": "^3.1.0", "express": "^4.17.1", - "fs-extra": "^8.1.0", + "fs-extra": "^10.0.0", "heapdump": "^0.3.15", "lockfile": "^1.0.4", "lodash": "^4.17.20", From e9736b12225dcfe1d439a0dcf76541c136aa54f3 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 17 May 2021 09:25:29 +0100 Subject: [PATCH 655/709] [perf] drop useless synchronous syscall on hot path for writing docs --- app/js/ResourceWriter.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index 1625ee15..d8cc2a4c 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -334,12 +334,7 @@ module.exports = ResourceWriter = { } ) // try and continue compiling even if http resource can not be downloaded at this time } else { - const process = require('process') fs.writeFile(path, resource.content, callback) - try { - let result - return (result = fs.lstatSync(path)) - } catch (e) {} } }) }) From 2c319c6cd3c21f4f6466ec01dc81c95d8a9a4ee8 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 17 May 2021 10:54:11 +0100 Subject: [PATCH 656/709] use fs.copyFile instead of fse.copy in UrlCache module --- app/js/UrlCache.js | 3 +-- test/unit/js/UrlCacheTests.js | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index 90c6486f..23afafaa 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -19,7 +19,6 @@ const UrlFetcher = require('./UrlFetcher') const Settings = require('settings-sharelatex') const crypto = require('crypto') const fs = require('fs') -const fse = require('fs-extra') const logger = require('logger-sharelatex') const async = require('async') @@ -36,7 +35,7 @@ module.exports = UrlCache = { if (error != null) { return callback(error) } - return fse.copy(pathToCachedUrl, destPath, function (error) { + return fs.copyFile(pathToCachedUrl, destPath, function (error) { if (error != null) { logger.error( { err: error, from: pathToCachedUrl, to: destPath }, diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index 7276b132..40652c58 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -27,8 +27,7 @@ describe('UrlCache', function () { 'settings-sharelatex': (this.Settings = { path: { clsiCacheDir: '/cache/dir' } }), - fs: (this.fs = {}), - 'fs-extra': (this.fse = { copy: sinon.stub().yields() }) + fs: (this.fs = { copyFile: sinon.stub().yields() }) } })) }) @@ -249,7 +248,6 @@ describe('UrlCache', function () { beforeEach(function () { this.cachePath = 'path/to/cached/url' this.destPath = 'path/to/destination' - this.UrlCache._copyFile = sinon.stub().callsArg(2) this.UrlCache._ensureUrlIsInCache = sinon .stub() .callsArgWith(3, null, this.cachePath) @@ -269,7 +267,7 @@ describe('UrlCache', function () { }) it('should copy the file to the new location', function () { - return this.fse.copy + return this.fs.copyFile .calledWith(this.cachePath, this.destPath) .should.equal(true) }) From a4d2e21adbdd68b16559135162a861b5cffc6db1 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 11 May 2021 15:53:01 +0100 Subject: [PATCH 657/709] add benchmark script for hashing --- test/bench/hashbench.js | 71 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 test/bench/hashbench.js diff --git a/test/bench/hashbench.js b/test/bench/hashbench.js new file mode 100644 index 00000000..9bb79c90 --- /dev/null +++ b/test/bench/hashbench.js @@ -0,0 +1,71 @@ +const ContentCacheManager = require('../../app/js/ContentCacheManager') +const fs = require('fs') +const crypto = require('crypto') +const path = require('path') +const os = require('os') +const async = require('async') +const _createHash = crypto.createHash + +const files = process.argv.slice(2) + +function test(hashType, filePath, callback) { + // override the default hash in ContentCacheManager + crypto.createHash = function (hash) { + if (hashType === 'hmac-sha1') { + return crypto.createHmac('sha1', 'a secret') + } + hash = hashType + return _createHash(hash) + } + fs.mkdtemp(path.join(os.tmpdir(), 'pdfcache'), (err, dir) => { + if (err) { + return callback(err) + } + const t0 = process.hrtime.bigint() + ContentCacheManager.update(dir, filePath, (x) => { + const t1 = process.hrtime.bigint() + const cold = Number(t1 - t0) / 1e6 + ContentCacheManager.update(dir, filePath, (x) => { + const t2 = process.hrtime.bigint() + const warm = Number(t2 - t1) / 1e6 + fs.rmdir(dir, { recursive: true }, (err) => { + if (err) { + return callback(err) + } + console.log( + filePath, + 'hashType', + hashType, + 'cold-start', + cold.toFixed(2), + 'ms', + 'warm-start', + warm.toFixed(2), + 'ms' + ) + callback(null, [hashType, cold, warm]) + }) + }) + }) + }) +} + +var jobs = [] +files.forEach((file) => { + jobs.push((cb) => { + test('md5', file, cb) + }) + jobs.push((cb) => { + test('sha1', file, cb) + }) + jobs.push((cb) => { + test('hmac-sha1', file, cb) + }) + jobs.push((cb) => { + test('sha256', file, cb) + }) +}) + +async.series(jobs, () => { + console.log('DONE') +}) From 834835148febbcc14c1a612e2791558ecde4dfe0 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 12 May 2021 16:22:59 +0100 Subject: [PATCH 658/709] run the hash benchmark 10 times --- test/bench/hashbench.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/bench/hashbench.js b/test/bench/hashbench.js index 9bb79c90..e87c3dd0 100644 --- a/test/bench/hashbench.js +++ b/test/bench/hashbench.js @@ -66,6 +66,13 @@ files.forEach((file) => { }) }) -async.series(jobs, () => { - console.log('DONE') -}) +async.timesSeries( + 10, + (n, cb) => { + console.log('run', n) + async.series(jobs, cb) + }, + () => { + console.log('DONE') + } +) From 7f090b10537c8eafcde48b9fbc563c75f852ac00 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 17 May 2021 12:00:32 +0100 Subject: [PATCH 659/709] include UV_THREADPOOL_SIZE in benchmark logs --- test/bench/hashbench.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/bench/hashbench.js b/test/bench/hashbench.js index e87c3dd0..a8ac898e 100644 --- a/test/bench/hashbench.js +++ b/test/bench/hashbench.js @@ -33,6 +33,8 @@ function test(hashType, filePath, callback) { return callback(err) } console.log( + 'uvthreads', + process.env.UV_THREADPOOL_SIZE, filePath, 'hashType', hashType, From 81526eafa2ec330c9b57e43bcce7d644904b121b Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Mon, 17 May 2021 12:00:57 +0100 Subject: [PATCH 660/709] remove unnecessary console.log from hash benchmark --- test/bench/hashbench.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/test/bench/hashbench.js b/test/bench/hashbench.js index a8ac898e..bbd9a028 100644 --- a/test/bench/hashbench.js +++ b/test/bench/hashbench.js @@ -68,13 +68,6 @@ files.forEach((file) => { }) }) -async.timesSeries( - 10, - (n, cb) => { - console.log('run', n) - async.series(jobs, cb) - }, - () => { - console.log('DONE') - } -) +async.timesSeries(10, (n, cb) => { + async.series(jobs, cb) +}) From 38e7825d8b2305ab4ddb54f152670cb48600ec65 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 17 May 2021 14:18:07 +0100 Subject: [PATCH 661/709] [ContentCacheManager] write streams to disk atomically Use an intermediate file for writing to disk, then rename to the target. --- app/js/ContentCacheManager.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 94ed9ebc..a32a952c 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -100,17 +100,27 @@ async function writePdfStream(dir, hash, buffers) { // ETags used for client side caching via browser internals. return false } catch (e) {} - const file = await fs.promises.open(filename, 'w') + const atomicWriteFilename = filename + '~' + const file = await fs.promises.open(atomicWriteFilename, 'w') if (Settings.enablePdfCachingDark) { // Write an empty file in dark mode. buffers = [] } try { - for (const buffer of buffers) { - await file.write(buffer) + try { + for (const buffer of buffers) { + await file.write(buffer) + } + } finally { + await file.close() + } + await fs.promises.rename(atomicWriteFilename, filename) + } catch (err) { + try { + await fs.promises.unlink(atomicWriteFilename) + } catch (_) { + throw err } - } finally { - await file.close() } return true } From 7d7a6346b42df95420bcbb30bc34017950d62566 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 17 May 2021 14:07:37 +0100 Subject: [PATCH 662/709] [ContentCacheManager] add support for stream detection across chunks Retain a small part (6 or 9 bytes) of each chunk in memory for providing the next iteration with enough context for finding the start/end marker of a range. --- app/js/ContentCacheManager.js | 31 ++++- test/unit/js/ContentCacheManagerTests.js | 160 +++++++++++++++++++++++ 2 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 test/unit/js/ContentCacheManagerTests.js diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 94ed9ebc..45fef178 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -10,6 +10,11 @@ const Settings = require('settings-sharelatex') const MIN_CHUNK_SIZE = Settings.pdfCachingMinChunkSize +const START_OF_STREAM_MARKER = 'stream' +const END_OF_STREAM_MARKER = 'endstream' +const START_OF_STREAM_MARKER_LENGTH = START_OF_STREAM_MARKER.length +const END_OF_STREAM_MARKER_LENGTH = END_OF_STREAM_MARKER.length + /** * * @param {String} contentDir path to directory where content hash files are cached @@ -41,15 +46,17 @@ class PdfStreamsExtractor { this.inStream = false this.streamStartIndex = 0 this.buffers = [] + this.lastChunk = Buffer.alloc(0) } consume(chunk) { let chunkIndex = 0 const pdfStreams = [] + chunk = Buffer.concat([this.lastChunk, chunk]) while (true) { if (!this.inStream) { // Not in a stream, look for stream start - const index = chunk.indexOf('stream', chunkIndex) + const index = chunk.indexOf(START_OF_STREAM_MARKER, chunkIndex) if (index === -1) { // Couldn't find stream start break @@ -60,13 +67,12 @@ class PdfStreamsExtractor { chunkIndex = index } else { // In a stream, look for stream end - const index = chunk.indexOf('endstream', chunkIndex) + const index = chunk.indexOf(END_OF_STREAM_MARKER, chunkIndex) if (index === -1) { - this.buffers.push(chunk.slice(chunkIndex)) break } // add "endstream" part - const endIndex = index + 9 + const endIndex = index + END_OF_STREAM_MARKER_LENGTH this.buffers.push(chunk.slice(chunkIndex, endIndex)) pdfStreams.push({ start: this.streamStartIndex, @@ -78,7 +84,22 @@ class PdfStreamsExtractor { chunkIndex = endIndex } } - this.fileIndex += chunk.length + + const remaining = chunk.length - chunkIndex + const nextMarkerLength = this.inStream + ? END_OF_STREAM_MARKER_LENGTH + : START_OF_STREAM_MARKER_LENGTH + if (remaining > nextMarkerLength) { + const retainMarkerSection = chunk.length - nextMarkerLength + if (this.inStream) { + this.buffers.push(chunk.slice(chunkIndex, retainMarkerSection)) + } + this.lastChunk = chunk.slice(retainMarkerSection) + this.fileIndex += retainMarkerSection + } else { + this.lastChunk = chunk.slice(chunkIndex) + this.fileIndex += chunkIndex + } return pdfStreams } } diff --git a/test/unit/js/ContentCacheManagerTests.js b/test/unit/js/ContentCacheManagerTests.js new file mode 100644 index 00000000..90fe60c5 --- /dev/null +++ b/test/unit/js/ContentCacheManagerTests.js @@ -0,0 +1,160 @@ +const Path = require('path') +const crypto = require('crypto') +const { Readable } = require('stream') +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const { expect } = require('chai') + +const MODULE_PATH = '../../../app/js/ContentCacheManager' + +class FakeFile { + constructor() { + this.closed = false + this.contents = [] + } + + async write(blob) { + this.contents.push(blob) + return this + } + + async close() { + this.closed = true + return this + } + + toJSON() { + return { + contents: Buffer.concat(this.contents).toString(), + closed: this.closed + } + } +} + +function hash(blob) { + const hash = crypto.createHash('sha256') + hash.update(blob) + return hash.digest('hex') +} + +describe('ContentCacheManager', function () { + let contentDir, pdfPath + let ContentCacheManager, fs, files, Settings + function load() { + ContentCacheManager = SandboxedModule.require(MODULE_PATH, { + requires: { + fs, + 'settings-sharelatex': Settings + } + }) + } + let contentRanges, newContentRanges + function run(filePath, done) { + ContentCacheManager.update(contentDir, filePath, (err, ranges) => { + if (err) return done(err) + ;[contentRanges, newContentRanges] = ranges + done() + }) + } + + beforeEach(function () { + contentDir = + '/app/output/602cee6f6460fca0ba7921e6/content/1797a7f48f9-5abc1998509dea1f' + pdfPath = + '/app/output/602cee6f6460fca0ba7921e6/generated-files/1797a7f48ea-8ac6805139f43351/output.pdf' + Settings = { + pdfCachingMinChunkSize: 1024, + enablePdfCachingDark: false + } + files = {} + fs = { + createReadStream: sinon.stub().returns(Readable.from([])), + promises: { + async open(name) { + files[name] = new FakeFile() + return files[name] + }, + async stat(name) { + if (!files[name]) { + throw new Error() + } + }, + rename: sinon.stub().resolves(), + unlink: sinon.stub().resolves() + } + } + }) + + describe('with a small minChunkSize', function () { + beforeEach(function () { + Settings.pdfCachingMinChunkSize = 1 + load() + }) + + describe('when the ranges are split across chunks', function () { + const RANGE_1 = 'stream123endstream' + const RANGE_2 = 'stream(|)endstream' + const RANGE_3 = 'stream!$%endstream' + beforeEach(function (done) { + fs.createReadStream + .withArgs(pdfPath) + .returns( + Readable.from([ + Buffer.from('abcstr'), + Buffer.from('eam123endstreamABC'), + Buffer.from('str'), + Buffer.from('eam(|'), + Buffer.from(')end'), + Buffer.from('stream-_~stream!$%endstream') + ]) + ) + run(pdfPath, done) + }) + + it('should produce three ranges', function () { + expect(contentRanges).to.have.length(3) + }) + + it('should find the correct offsets', function () { + expect(contentRanges).to.deep.equal([ + { + start: 3, + end: 21, + hash: hash(RANGE_1) + }, + { + start: 24, + end: 42, + hash: hash(RANGE_2) + }, + { + start: 45, + end: 63, + hash: hash(RANGE_3) + } + ]) + }) + + it('should store the contents', function () { + expect(JSON.parse(JSON.stringify(files))).to.deep.equal({ + [Path.join(contentDir, hash(RANGE_1))]: { + contents: RANGE_1, + closed: true + }, + [Path.join(contentDir, hash(RANGE_2))]: { + contents: RANGE_2, + closed: true + }, + [Path.join(contentDir, hash(RANGE_3))]: { + contents: RANGE_3, + closed: true + } + }) + }) + + it('should mark all ranges as new', function () { + expect(contentRanges).to.deep.equal(newContentRanges) + }) + }) + }) +}) From 979a1d465ae5ad105dbc98ac15ad1e74142a6028 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 18 May 2021 09:50:13 +0100 Subject: [PATCH 663/709] [ContentCacheManager] skip writing of duplicate streams --- app/js/ContentCacheManager.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 94ed9ebc..f631fa6a 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -20,13 +20,20 @@ async function update(contentDir, filePath) { const extractor = new PdfStreamsExtractor() const ranges = [] const newRanges = [] + const seenHashes = new Set() for await (const chunk of stream) { const pdfStreams = extractor.consume(chunk) for (const pdfStream of pdfStreams) { if (pdfStream.end - pdfStream.start < MIN_CHUNK_SIZE) continue const hash = pdfStreamHash(pdfStream.buffers) + const range = { start: pdfStream.start, end: pdfStream.end, hash } ranges.push(range) + + // Optimization: Skip writing of duplicate streams. + if (seenHashes.has(hash)) continue + seenHashes.add(hash) + if (await writePdfStream(contentDir, hash, pdfStream.buffers)) { newRanges.push(range) } From 2a366cf317c1ac67391f7d9c7af41840845b3348 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 18 May 2021 11:09:30 +0100 Subject: [PATCH 664/709] [misc] fix unit tests following the merge of atomic writes --- test/unit/js/ContentCacheManagerTests.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/unit/js/ContentCacheManagerTests.js b/test/unit/js/ContentCacheManagerTests.js index 90fe60c5..a7b25eac 100644 --- a/test/unit/js/ContentCacheManagerTests.js +++ b/test/unit/js/ContentCacheManagerTests.js @@ -79,7 +79,13 @@ describe('ContentCacheManager', function () { throw new Error() } }, - rename: sinon.stub().resolves(), + async rename(oldName, newName) { + if (!files[oldName]) { + throw new Error() + } + files[newName] = files[oldName] + delete files[oldName] + }, unlink: sinon.stub().resolves() } } From ebcfba4929a3e8ba1d48dec9132e7d9f1a343679 Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Tue, 18 May 2021 16:25:24 +0100 Subject: [PATCH 665/709] wip expire old hash files --- app/js/ContentCacheManager.js | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 2096e1af..f5ccc503 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -26,6 +26,10 @@ async function update(contentDir, filePath) { const ranges = [] const newRanges = [] const seenHashes = new Set() + // keep track of hashes expire old ones when they reach a generation > N. + const tracker = new HashFileTracker() + await loadState(contentDir, tracker) + for await (const chunk of stream) { const pdfStreams = extractor.consume(chunk) for (const pdfStream of pdfStreams) { @@ -44,9 +48,45 @@ async function update(contentDir, filePath) { } } } + const expiredHashes = tracker.update(ranges).findStale(5) + await deleteHashFiles(expiredHashes) return [ranges, newRanges] } +class HashFileTracker { + constructor(contentDir) { + this.hashAge = new Map() + } + + update(ranges) { + for (const [hash, age] of this.hashAge) { + this.hashAge.set(hash, age + 1) + } + for (const range in ranges) { + this.hashAge.set(range.hash, 0) + } + } + + findStale(maxAge) { + var stale = [] + for (const [hash, age] of this.hashAge) { + if (age > maxAge) { + stale.push(hash) + this.hashAge.delete(hash) + } + } + return stale + } +} + +async function loadState(contentDir, tracker) { + +} + +async function deleteHashFiles(n) { + // delete any hash file older than N generations +} + class PdfStreamsExtractor { constructor() { this.fileIndex = 0 From 3897418ce0c954007d901015f31b93304aaf2360 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 18 May 2021 17:32:21 +0100 Subject: [PATCH 666/709] [misc] install p-limit and nodemon --- package-lock.json | 812 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 809 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a662040..773d9f43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1066,6 +1066,12 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, "@sinonjs/commons": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", @@ -1127,6 +1133,15 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -1276,6 +1291,55 @@ "uri-js": "^4.2.2" } }, + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "ansi-colors": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", @@ -1587,6 +1651,105 @@ "integrity": "sha1-tcCeF8rNET0Rt7s+04TMASmU2Gs=", "dev": true }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1652,6 +1815,38 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + } + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1760,6 +1955,18 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", "integrity": "sha1-VHJri4//TfBTxCGH6AH7RBLfFJQ=" }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -1826,6 +2033,15 @@ } } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "cls-bluebird": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", @@ -1931,6 +2147,20 @@ } } }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -2014,6 +2244,12 @@ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, "d64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz", @@ -2047,6 +2283,15 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -2067,6 +2312,12 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -2206,6 +2457,12 @@ } } }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -2315,6 +2572,12 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3170,6 +3433,15 @@ "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", "dev": true }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -3200,6 +3472,23 @@ "is-glob": "^4.0.1" } }, + "global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "dev": true, + "requires": { + "ini": "1.3.7" + }, + "dependencies": { + "ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "dev": true + } + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -3418,6 +3707,25 @@ "node-forge": "^0.9.0" } }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, "graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", @@ -3509,6 +3817,12 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3541,6 +3855,12 @@ "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", "dev": true }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -3647,6 +3967,12 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, "ignore-walk": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", @@ -3665,6 +3991,12 @@ "resolve-from": "^4.0.0" } }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3874,6 +4206,15 @@ "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "dev": true }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", @@ -3903,6 +4244,22 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dev": true, + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3914,6 +4271,12 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -3959,6 +4322,12 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -4010,6 +4379,12 @@ "bignumber.js": "^7.0.0" } }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -4086,6 +4461,24 @@ "safe-buffer": "^5.0.1" } }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "requires": { + "package-json": "^6.3.0" + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -4286,6 +4679,12 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4294,6 +4693,23 @@ "yallist": "^3.0.2" } }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "make-plural": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz", @@ -4396,6 +4812,12 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -4528,6 +4950,17 @@ "dev": true, "requires": { "p-limit": "^2.0.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } } } @@ -4750,6 +5183,56 @@ "tar": "^4" } }, + "nodemon": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", + "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", + "dev": true, + "requires": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", @@ -4777,6 +5260,12 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, "npm-bundled": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", @@ -4950,13 +5439,18 @@ "os-tmpdir": "^1.0.0" } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { @@ -4991,6 +5485,26 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5226,6 +5740,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, "prettier": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", @@ -5791,6 +6311,17 @@ "dev": true, "requires": { "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } }, "path-exists": { @@ -5929,6 +6460,12 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -5976,6 +6513,15 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=" }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "requires": { + "escape-goat": "^2.0.0" + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -6133,6 +6679,24 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -6239,6 +6803,15 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -6343,6 +6916,23 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "integrity": "sha1-fnQlb7qknHWqfHogXMInmcrIAAQ=" }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", @@ -6952,6 +7542,12 @@ } } }, + "term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7002,6 +7598,12 @@ "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=" }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7037,6 +7639,26 @@ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -7091,17 +7713,44 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { "version": "3.7.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", "dev": true }, + "undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "dev": true, + "requires": { + "debug": "^2.2.0" + } + }, "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -7112,6 +7761,78 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "dev": true, + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -7120,6 +7841,15 @@ "punycode": "^2.1.0" } }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7314,6 +8044,49 @@ "string-width": "^1.0.2 || 2" } }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "requires": { + "string-width": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "wkx": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz", @@ -7409,6 +8182,24 @@ } } }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true + }, "xregexp": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", @@ -7491,6 +8282,17 @@ "dev": true, "requires": { "p-limit": "^2.0.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } }, "string-width": { diff --git a/package.json b/package.json index 3afa29c4..dac3ba9d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lodash": "^4.17.20", "logger-sharelatex": "^2.2.0", "mysql": "^2.18.1", + "p-limit": "^3.1.0", "request": "^2.88.2", "sequelize": "^5.21.5", "settings-sharelatex": "^1.1.0", @@ -58,6 +59,7 @@ "eslint-plugin-react": "^7.19.0", "eslint-plugin-standard": "^4.0.1", "mocha": "^7.1.0", + "nodemon": "^2.0.7", "prettier": "^2.0.0", "prettier-eslint-cli": "^5.0.0", "sandboxed-module": "^2.0.3", From 66974ddd2f7da50c605152eb3234be9f9388945a Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 18 May 2021 18:06:15 +0100 Subject: [PATCH 667/709] [ContentCacheManager] finish tracking of ranges across builds --- app/js/ContentCacheManager.js | 93 +++++++++-- app/js/ContentCacheMetrics.js | 5 +- app/js/OutputCacheManager.js | 5 +- test/unit/js/ContentCacheManagerTests.js | 203 +++++++++++++++++++++-- 4 files changed, 272 insertions(+), 34 deletions(-) diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index f5ccc503..60c68b9b 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -7,6 +7,7 @@ const fs = require('fs') const crypto = require('crypto') const Path = require('path') const Settings = require('settings-sharelatex') +const pLimit = require('p-limit') const MIN_CHUNK_SIZE = Settings.pdfCachingMinChunkSize @@ -27,9 +28,7 @@ async function update(contentDir, filePath) { const newRanges = [] const seenHashes = new Set() // keep track of hashes expire old ones when they reach a generation > N. - const tracker = new HashFileTracker() - await loadState(contentDir, tracker) - + const tracker = await HashFileTracker.from(contentDir) for await (const chunk of stream) { const pdfStreams = extractor.consume(chunk) for (const pdfStream of pdfStreams) { @@ -48,43 +47,98 @@ async function update(contentDir, filePath) { } } } - const expiredHashes = tracker.update(ranges).findStale(5) - await deleteHashFiles(expiredHashes) - return [ranges, newRanges] + tracker.update(ranges, newRanges) + const reclaimedSpace = await tracker.deleteStaleHashes(5) + await tracker.flush() + return [ranges, newRanges, reclaimedSpace] +} + +function getStatePath(contentDir) { + return Path.join(contentDir, '.state.v0.json') } class HashFileTracker { - constructor(contentDir) { - this.hashAge = new Map() + constructor(contentDir, { hashAge = [], hashSize = [] }) { + this.contentDir = contentDir + this.hashAge = new Map(hashAge) + this.hashSize = new Map(hashSize) + } + + static async from(contentDir) { + const statePath = getStatePath(contentDir) + let state = {} + try { + const blob = await fs.promises.readFile(statePath) + state = JSON.parse(blob) + } catch (e) {} + return new HashFileTracker(contentDir, state) } - update(ranges) { + update(ranges, newRanges) { for (const [hash, age] of this.hashAge) { this.hashAge.set(hash, age + 1) } - for (const range in ranges) { + for (const range of ranges) { this.hashAge.set(range.hash, 0) } + for (const range of newRanges) { + this.hashSize.set(range.hash, range.end - range.start) + } + return this } findStale(maxAge) { - var stale = [] + const stale = [] for (const [hash, age] of this.hashAge) { if (age > maxAge) { stale.push(hash) - this.hashAge.delete(hash) } } return stale } -} -async function loadState(contentDir, tracker) { + async flush() { + const statePath = getStatePath(this.contentDir) + const blob = JSON.stringify({ + hashAge: Array.from(this.hashAge.entries()), + hashSize: Array.from(this.hashSize.entries()) + }) + const atomicWrite = statePath + '~' + try { + await fs.promises.writeFile(atomicWrite, blob) + } catch (err) { + try { + await fs.promises.unlink(atomicWrite) + } catch (e) {} + throw err + } + try { + await fs.promises.rename(atomicWrite, statePath) + } catch (err) { + try { + await fs.promises.unlink(atomicWrite) + } catch (e) {} + throw err + } + } + + async deleteStaleHashes(n) { + // delete any hash file older than N generations + const hashes = this.findStale(n) -} + let reclaimedSpace = 0 + if (hashes.length === 0) { + return reclaimedSpace + } -async function deleteHashFiles(n) { - // delete any hash file older than N generations + await promiseMapWithLimit(10, hashes, async (hash) => { + await fs.promises.unlink(Path.join(this.contentDir, hash)) + this.hashAge.delete(hash) + reclaimedSpace += this.hashSize.get(hash) + this.hashSize.delete(hash) + }) + return reclaimedSpace + } } class PdfStreamsExtractor { @@ -193,6 +247,11 @@ async function writePdfStream(dir, hash, buffers) { return true } +function promiseMapWithLimit(concurrency, array, fn) { + const limit = pLimit(concurrency) + return Promise.all(array.map((x) => limit(() => fn(x)))) +} + module.exports = { HASH_REGEX: /^[0-9a-f]{64}$/, update: callbackify(update) diff --git a/app/js/ContentCacheMetrics.js b/app/js/ContentCacheMetrics.js index 04b475f8..f5f9188d 100644 --- a/app/js/ContentCacheMetrics.js +++ b/app/js/ContentCacheMetrics.js @@ -72,7 +72,10 @@ function emitPdfCachingStats(stats, timings) { // How much space do the ranges use? // This will accumulate the ranges size over time, skipping already written ranges. - Metrics.summary('pdf-ranges-disk-size', stats['pdf-caching-new-ranges-size']) + Metrics.summary( + 'pdf-ranges-disk-size', + stats['pdf-caching-new-ranges-size'] - stats['pdf-caching-reclaimed-space'] + ) } module.exports = { diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index fe23ae3e..e6167570 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -278,10 +278,10 @@ module.exports = OutputCacheManager = { const timer = new Metrics.Timer('compute-pdf-ranges') ContentCacheManager.update(contentDir, outputFilePath, function ( err, - ranges + result ) { if (err) return callback(err, outputFiles) - const [contentRanges, newContentRanges] = ranges + const [contentRanges, newContentRanges, reclaimedSpace] = result if (Settings.enablePdfCachingDark) { // In dark mode we are doing the computation only and do not emit @@ -302,6 +302,7 @@ module.exports = OutputCacheManager = { (sum, next) => sum + (next.end - next.start), 0 ) + stats['pdf-caching-reclaimed-space'] = reclaimedSpace callback(null, outputFiles) }) } else { diff --git a/test/unit/js/ContentCacheManagerTests.js b/test/unit/js/ContentCacheManagerTests.js index a7b25eac..35f22fff 100644 --- a/test/unit/js/ContentCacheManagerTests.js +++ b/test/unit/js/ContentCacheManagerTests.js @@ -48,16 +48,19 @@ describe('ContentCacheManager', function () { } }) } - let contentRanges, newContentRanges + let contentRanges, newContentRanges, reclaimed function run(filePath, done) { ContentCacheManager.update(contentDir, filePath, (err, ranges) => { if (err) return done(err) - ;[contentRanges, newContentRanges] = ranges + let newlyReclaimed + ;[contentRanges, newContentRanges, newlyReclaimed] = ranges + reclaimed += newlyReclaimed done() }) } beforeEach(function () { + reclaimed = 0 contentDir = '/app/output/602cee6f6460fca0ba7921e6/content/1797a7f48f9-5abc1998509dea1f' pdfPath = @@ -70,6 +73,18 @@ describe('ContentCacheManager', function () { fs = { createReadStream: sinon.stub().returns(Readable.from([])), promises: { + async writeFile(name, blob) { + const file = new FakeFile() + await file.write(Buffer.from(blob)) + await file.close() + files[name] = file + }, + async readFile(name) { + if (!files[name]) { + throw new Error() + } + return files[name].toJSON().contents + }, async open(name) { files[name] = new FakeFile() return files[name] @@ -86,7 +101,12 @@ describe('ContentCacheManager', function () { files[newName] = files[oldName] delete files[oldName] }, - unlink: sinon.stub().resolves() + async unlink(name) { + if (!files[name]) { + throw new Error() + } + delete files[name] + } } } }) @@ -99,9 +119,12 @@ describe('ContentCacheManager', function () { describe('when the ranges are split across chunks', function () { const RANGE_1 = 'stream123endstream' - const RANGE_2 = 'stream(|)endstream' - const RANGE_3 = 'stream!$%endstream' - beforeEach(function (done) { + const RANGE_2 = 'stream(||)endstream' + const RANGE_3 = 'stream!$%/=endstream' + const h1 = hash(RANGE_1) + const h2 = hash(RANGE_2) + const h3 = hash(RANGE_3) + function runWithSplitStream(done) { fs.createReadStream .withArgs(pdfPath) .returns( @@ -109,12 +132,15 @@ describe('ContentCacheManager', function () { Buffer.from('abcstr'), Buffer.from('eam123endstreamABC'), Buffer.from('str'), - Buffer.from('eam(|'), + Buffer.from('eam(||'), Buffer.from(')end'), - Buffer.from('stream-_~stream!$%endstream') + Buffer.from('stream-_~stream!$%/=endstream') ]) ) run(pdfPath, done) + } + beforeEach(function (done) { + runWithSplitStream(done) }) it('should produce three ranges', function () { @@ -130,12 +156,12 @@ describe('ContentCacheManager', function () { }, { start: 24, - end: 42, + end: 43, hash: hash(RANGE_2) }, { - start: 45, - end: 63, + start: 46, + end: 66, hash: hash(RANGE_3) } ]) @@ -143,17 +169,32 @@ describe('ContentCacheManager', function () { it('should store the contents', function () { expect(JSON.parse(JSON.stringify(files))).to.deep.equal({ - [Path.join(contentDir, hash(RANGE_1))]: { + [Path.join(contentDir, h1)]: { contents: RANGE_1, closed: true }, - [Path.join(contentDir, hash(RANGE_2))]: { + [Path.join(contentDir, h2)]: { contents: RANGE_2, closed: true }, - [Path.join(contentDir, hash(RANGE_3))]: { + [Path.join(contentDir, h3)]: { contents: RANGE_3, closed: true + }, + [Path.join(contentDir, '.state.v0.json')]: { + contents: JSON.stringify({ + hashAge: [ + [h1, 0], + [h2, 0], + [h3, 0] + ], + hashSize: [ + [h1, 18], + [h2, 19], + [h3, 20] + ] + }), + closed: true } }) }) @@ -161,6 +202,140 @@ describe('ContentCacheManager', function () { it('should mark all ranges as new', function () { expect(contentRanges).to.deep.equal(newContentRanges) }) + + describe('when re-running with one stream removed', function () { + function runWithOneSplitStreamRemoved(done) { + fs.createReadStream + .withArgs(pdfPath) + .returns( + Readable.from([ + Buffer.from('abcstr'), + Buffer.from('eam123endstreamABC'), + Buffer.from('stream!$%/=endstream') + ]) + ) + run(pdfPath, done) + } + beforeEach(function (done) { + runWithOneSplitStreamRemoved(done) + }) + + it('should produce two ranges', function () { + expect(contentRanges).to.have.length(2) + }) + + it('should find the correct offsets', function () { + expect(contentRanges).to.deep.equal([ + { + start: 3, + end: 21, + hash: hash(RANGE_1) + }, + { + start: 24, + end: 44, + hash: hash(RANGE_3) + } + ]) + }) + + it('should update the age of the 2nd range', function () { + expect(JSON.parse(JSON.stringify(files))).to.deep.equal({ + [Path.join(contentDir, h1)]: { + contents: RANGE_1, + closed: true + }, + [Path.join(contentDir, h2)]: { + contents: RANGE_2, + closed: true + }, + [Path.join(contentDir, h3)]: { + contents: RANGE_3, + closed: true + }, + [Path.join(contentDir, '.state.v0.json')]: { + contents: JSON.stringify({ + hashAge: [ + [h1, 0], + [h2, 1], + [h3, 0] + ], + hashSize: [ + [h1, 18], + [h2, 19], + [h3, 20] + ] + }), + closed: true + } + }) + }) + + it('should find no new ranges', function () { + expect(newContentRanges).to.deep.equal([]) + }) + + describe('when re-running 5 more times', function () { + for (let i = 0; i < 5; i++) { + beforeEach(function (done) { + runWithOneSplitStreamRemoved(done) + }) + } + + it('should still produce two ranges', function () { + expect(contentRanges).to.have.length(2) + }) + + it('should still find the correct offsets', function () { + expect(contentRanges).to.deep.equal([ + { + start: 3, + end: 21, + hash: hash(RANGE_1) + }, + { + start: 24, + end: 44, + hash: hash(RANGE_3) + } + ]) + }) + + it('should delete the 2nd range', function () { + expect(JSON.parse(JSON.stringify(files))).to.deep.equal({ + [Path.join(contentDir, h1)]: { + contents: RANGE_1, + closed: true + }, + [Path.join(contentDir, h3)]: { + contents: RANGE_3, + closed: true + }, + [Path.join(contentDir, '.state.v0.json')]: { + contents: JSON.stringify({ + hashAge: [ + [h1, 0], + [h3, 0] + ], + hashSize: [ + [h1, 18], + [h3, 20] + ] + }), + closed: true + } + }) + }) + + it('should find no new ranges', function () { + expect(newContentRanges).to.deep.equal([]) + }) + + it('should yield the reclaimed space', function () { + expect(reclaimed).to.equal(RANGE_2.length) + }) + }) + }) }) }) }) From ab042dedd82e0807b4dcbde537a8c2c48c9264d2 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 18 May 2021 18:15:58 +0100 Subject: [PATCH 668/709] [ContentCacheManager] deeply integrate the HashFileTracker - Update the tracker contents as we hash ranges - Let the tracker be authoritative for already written ranges This is saving a stat call per newly written range. --- app/js/ContentCacheManager.js | 37 ++++++++++++++--------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 60c68b9b..d4246816 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -26,9 +26,10 @@ async function update(contentDir, filePath) { const extractor = new PdfStreamsExtractor() const ranges = [] const newRanges = [] - const seenHashes = new Set() // keep track of hashes expire old ones when they reach a generation > N. const tracker = await HashFileTracker.from(contentDir) + tracker.updateAge() + for await (const chunk of stream) { const pdfStreams = extractor.consume(chunk) for (const pdfStream of pdfStreams) { @@ -39,15 +40,12 @@ async function update(contentDir, filePath) { ranges.push(range) // Optimization: Skip writing of duplicate streams. - if (seenHashes.has(hash)) continue - seenHashes.add(hash) + if (tracker.track(range)) continue - if (await writePdfStream(contentDir, hash, pdfStream.buffers)) { - newRanges.push(range) - } + await writePdfStream(contentDir, hash, pdfStream.buffers) + newRanges.push(range) } } - tracker.update(ranges, newRanges) const reclaimedSpace = await tracker.deleteStaleHashes(5) await tracker.flush() return [ranges, newRanges, reclaimedSpace] @@ -74,16 +72,19 @@ class HashFileTracker { return new HashFileTracker(contentDir, state) } - update(ranges, newRanges) { + track(range) { + const exists = this.hashAge.has(range.hash) + if (!exists) { + this.hashSize.set(range.hash, range.end - range.start) + } + this.hashAge.set(range.hash, 0) + return exists + } + + updateAge() { for (const [hash, age] of this.hashAge) { this.hashAge.set(hash, age + 1) } - for (const range of ranges) { - this.hashAge.set(range.hash, 0) - } - for (const range of newRanges) { - this.hashSize.set(range.hash, range.end - range.start) - } return this } @@ -215,13 +216,6 @@ function pdfStreamHash(buffers) { async function writePdfStream(dir, hash, buffers) { const filename = Path.join(dir, hash) - try { - await fs.promises.stat(filename) - // The file exists. Do not rewrite the content. - // It would change the modified-time of the file and hence invalidate the - // ETags used for client side caching via browser internals. - return false - } catch (e) {} const atomicWriteFilename = filename + '~' const file = await fs.promises.open(atomicWriteFilename, 'w') if (Settings.enablePdfCachingDark) { @@ -244,7 +238,6 @@ async function writePdfStream(dir, hash, buffers) { throw err } } - return true } function promiseMapWithLimit(concurrency, array, fn) { From 3d20a2fe1646673f4723c5461adbdabb0d422891 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 19 May 2021 11:17:08 +0100 Subject: [PATCH 669/709] [LatexRunner] do not emit empty cpu/sys timings --- app/js/LatexRunner.js | 38 +++++++++++-------------- test/unit/js/LatexRunnerTests.js | 49 ++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index f8799d21..48000219 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -23,6 +23,12 @@ const fs = require('fs') const ProcessTable = {} // table of currently running jobs (pids or docker container names) +const TIME_V_METRICS = Object.entries({ + 'cpu-percent': /Percent of CPU this job got: (\d+)/m, + 'cpu-time': /User time.*: (\d+.\d+)/m, + 'sys-time': /System time.*: (\d+.\d+)/m +}) + module.exports = LatexRunner = { runLatex(project_id, options, callback) { let command @@ -116,28 +122,16 @@ module.exports = LatexRunner = { stats[`latex-runs-with-errors-${runs}`] = failed ? 1 : 0 // timing information from /usr/bin/time const timings = {} - const stderr = output != null ? output.stderr : undefined - timings['cpu-percent'] = - __guard__( - stderr != null - ? stderr.match(/Percent of CPU this job got: (\d+)/m) - : undefined, - (x3) => x3[1] - ) || 0 - timings['cpu-time'] = - __guard__( - stderr != null - ? stderr.match(/User time.*: (\d+.\d+)/m) - : undefined, - (x4) => x4[1] - ) || 0 - timings['sys-time'] = - __guard__( - stderr != null - ? stderr.match(/System time.*: (\d+.\d+)/m) - : undefined, - (x5) => x5[1] - ) || 0 + const stderr = (output && output.stderr) || '' + if (stderr.includes('Command being timed:')) { + // Add metrics for runs with `$ time -v ...` + for (const [timing, matcher] of TIME_V_METRICS) { + const match = stderr.match(matcher) + if (match) { + timings[timing] = parseFloat(match[1]) + } + } + } // record output files LatexRunner.writeLogOutput(project_id, directory, output, () => { return callback(error, output, stats, timings) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index a9116d0f..f763f39c 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -11,6 +11,7 @@ */ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') +const { expect } = require('chai') const modulePath = require('path').join( __dirname, '../../../app/js/LatexRunner' @@ -58,7 +59,7 @@ describe('LatexRunner', function () { }) describe('normally', function () { - beforeEach(function () { + beforeEach(function (done) { return this.LatexRunner.runLatex( this.project_id, { @@ -70,7 +71,10 @@ describe('LatexRunner', function () { environment: this.env, compileGroup: this.compileGroup }, - this.callback + (error, output, stats, timings) => { + this.timings = timings + done(error) + } ) }) @@ -96,6 +100,47 @@ describe('LatexRunner', function () { .calledWith(this.directory + '/' + 'output.stderr', 'this is stderr') .should.equal(true) }) + + it('should not record cpu metrics', function () { + expect(this.timings['cpu-percent']).to.not.exist + expect(this.timings['cpu-time']).to.not.exist + expect(this.timings['sys-time']).to.not.exist + }) + }) + + describe('with time -v', function () { + beforeEach(function (done) { + this.CommandRunner.run = sinon.stub().callsArgWith(7, null, { + stdout: 'this is stdout', + stderr: + '\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' + + '\tUser time (seconds): 0.28\n' + + '\tSystem time (seconds): 0.70\n' + + '\tPercent of CPU this job got: 98%\n' + }) + this.LatexRunner.runLatex( + this.project_id, + { + directory: this.directory, + mainFile: this.mainFile, + compiler: this.compiler, + timeout: (this.timeout = 42000), + image: this.image, + environment: this.env, + compileGroup: this.compileGroup + }, + (error, output, stats, timings) => { + this.timings = timings + done(error) + } + ) + }) + + it('should record cpu metrics', function () { + expect(this.timings['cpu-percent']).to.equal(98) + expect(this.timings['cpu-time']).to.equal(0.28) + expect(this.timings['sys-time']).to.equal(0.7) + }) }) describe('with an .Rtex main file', function () { From 8066e3ec031a9bb9311019b38db40913d3e16818 Mon Sep 17 00:00:00 2001 From: decaffeinate <alf.eaton@overleaf.com> Date: Wed, 19 May 2021 16:50:45 +0100 Subject: [PATCH 670/709] decaffeinate: Rename settings.test.coffee from .coffee to .js --- .../acceptance/scripts/{settings.test.coffee => settings.test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/acceptance/scripts/{settings.test.coffee => settings.test.js} (100%) diff --git a/test/acceptance/scripts/settings.test.coffee b/test/acceptance/scripts/settings.test.js similarity index 100% rename from test/acceptance/scripts/settings.test.coffee rename to test/acceptance/scripts/settings.test.js From 9356d8455c2500d64833b32ea57ad5c5475b7a1f Mon Sep 17 00:00:00 2001 From: decaffeinate <alf.eaton@overleaf.com> Date: Wed, 19 May 2021 16:50:45 +0100 Subject: [PATCH 671/709] decaffeinate: Convert settings.test.coffee to JS --- test/acceptance/scripts/settings.test.js | 78 ++++++++++++++---------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/test/acceptance/scripts/settings.test.js b/test/acceptance/scripts/settings.test.js index e3d3b70b..38a6e034 100644 --- a/test/acceptance/scripts/settings.test.js +++ b/test/acceptance/scripts/settings.test.js @@ -1,47 +1,59 @@ -Path = require "path" +const Path = require("path"); -module.exports = - # Options are passed to Sequelize. - # See http://sequelizejs.com/documentation#usage-options for details - mysql: - clsi: - database: "clsi" - username: "clsi" - password: null - dialect: "sqlite" +module.exports = { + // Options are passed to Sequelize. + // See http://sequelizejs.com/documentation#usage-options for details + mysql: { + clsi: { + database: "clsi", + username: "clsi", + password: null, + dialect: "sqlite", storage: Path.resolve("db.sqlite") + } + }, - path: - compilesDir: Path.resolve(__dirname + "/../../../compiles") - clsiCacheDir: Path.resolve(__dirname + "/../../../cache") - #synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) - synctexBaseDir: () -> "/compile" + path: { + compilesDir: Path.resolve(__dirname + "/../../../compiles"), + clsiCacheDir: Path.resolve(__dirname + "/../../../cache"), + //synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) + synctexBaseDir() { return "/compile"; }, sandboxedCompilesHostDir: process.env['SANDBOXED_COMPILES_HOST_DIR'] + }, - clsi: - #strace: true - #archive_logs: true - commandRunner: "docker-runner-sharelatex" - latexmkCommandPrefix: ["/usr/bin/time", "-v"] # on Linux - docker: - image: process.env.TEXLIVE_IMAGE || "texlive-full:2017.1-opt" - env: - PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/" + clsi: { + //strace: true + //archive_logs: true + commandRunner: "docker-runner-sharelatex", + latexmkCommandPrefix: ["/usr/bin/time", "-v"], // on Linux + docker: { + image: process.env.TEXLIVE_IMAGE || "texlive-full:2017.1-opt", + env: { + PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/", HOME: "/tmp" - modem: + }, + modem: { socketPath: false + }, user: process.env.SIBLING_CONTAINER_USER ||"111" + } + }, - internal: - clsi: - port: 3013 - load_port: 3044 + internal: { + clsi: { + port: 3013, + load_port: 3044, host: "localhost" + } + }, - apis: - clsi: + apis: { + clsi: { url: "http://localhost:3013" + } + }, - smokeTest: false - project_cache_length_ms: 1000 * 60 * 60 * 24 + smokeTest: false, + project_cache_length_ms: 1000 * 60 * 60 * 24, parallelFileDownloads:1 +}; From 78ae0a948527299411df68749592f46d3dab140b Mon Sep 17 00:00:00 2001 From: decaffeinate <alf.eaton@overleaf.com> Date: Wed, 19 May 2021 16:50:46 +0100 Subject: [PATCH 672/709] decaffeinate: Run post-processing cleanups on settings.test.coffee --- test/acceptance/scripts/settings.test.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/acceptance/scripts/settings.test.js b/test/acceptance/scripts/settings.test.js index 38a6e034..2f0cc4a1 100644 --- a/test/acceptance/scripts/settings.test.js +++ b/test/acceptance/scripts/settings.test.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-path-concat, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. const Path = require("path"); module.exports = { @@ -16,14 +21,14 @@ module.exports = { path: { compilesDir: Path.resolve(__dirname + "/../../../compiles"), clsiCacheDir: Path.resolve(__dirname + "/../../../cache"), - //synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) + // synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) synctexBaseDir() { return "/compile"; }, - sandboxedCompilesHostDir: process.env['SANDBOXED_COMPILES_HOST_DIR'] + sandboxedCompilesHostDir: process.env.SANDBOXED_COMPILES_HOST_DIR }, clsi: { - //strace: true - //archive_logs: true + // strace: true + // archive_logs: true commandRunner: "docker-runner-sharelatex", latexmkCommandPrefix: ["/usr/bin/time", "-v"], // on Linux docker: { From 21861441f2d69da548f92f9f2dba2508e65a5961 Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Wed, 19 May 2021 18:36:27 +0100 Subject: [PATCH 673/709] Tidy and format --- test/acceptance/scripts/settings.test.js | 114 +++++++++++------------ 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/test/acceptance/scripts/settings.test.js b/test/acceptance/scripts/settings.test.js index 2f0cc4a1..e960ce8b 100644 --- a/test/acceptance/scripts/settings.test.js +++ b/test/acceptance/scripts/settings.test.js @@ -1,64 +1,64 @@ -/* eslint-disable - no-path-concat, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -const Path = require("path"); +const Path = require('path') module.exports = { - // Options are passed to Sequelize. - // See http://sequelizejs.com/documentation#usage-options for details - mysql: { - clsi: { - database: "clsi", - username: "clsi", - password: null, - dialect: "sqlite", - storage: Path.resolve("db.sqlite") - } - }, + // Options are passed to Sequelize. + // See http://sequelizejs.com/documentation#usage-options for details + mysql: { + clsi: { + database: 'clsi', + username: 'clsi', + password: null, + dialect: 'sqlite', + storage: Path.resolve('db.sqlite'), + }, + }, - path: { - compilesDir: Path.resolve(__dirname + "/../../../compiles"), - clsiCacheDir: Path.resolve(__dirname + "/../../../cache"), - // synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) - synctexBaseDir() { return "/compile"; }, - sandboxedCompilesHostDir: process.env.SANDBOXED_COMPILES_HOST_DIR - }, + path: { + // eslint-disable-next-line no-path-concat + compilesDir: Path.resolve(__dirname + '/../../../compiles'), + // eslint-disable-next-line no-path-concat + clsiCacheDir: Path.resolve(__dirname + '/../../../cache'), + // synctexBaseDir: (project_id) -> Path.join(@compilesDir, project_id) + synctexBaseDir() { + return '/compile' + }, + sandboxedCompilesHostDir: process.env.SANDBOXED_COMPILES_HOST_DIR, + }, - clsi: { - // strace: true - // archive_logs: true - commandRunner: "docker-runner-sharelatex", - latexmkCommandPrefix: ["/usr/bin/time", "-v"], // on Linux - docker: { - image: process.env.TEXLIVE_IMAGE || "texlive-full:2017.1-opt", - env: { - PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/", - HOME: "/tmp" - }, - modem: { - socketPath: false - }, - user: process.env.SIBLING_CONTAINER_USER ||"111" - } - }, + clsi: { + // strace: true + // archive_logs: true + commandRunner: 'docker-runner-sharelatex', + latexmkCommandPrefix: ['/usr/bin/time', '-v'], // on Linux + docker: { + image: process.env.TEXLIVE_IMAGE || 'texlive-full:2017.1-opt', + env: { + PATH: + '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/', + HOME: '/tmp', + }, + modem: { + socketPath: false, + }, + user: process.env.SIBLING_CONTAINER_USER || '111', + }, + }, - internal: { - clsi: { - port: 3013, - load_port: 3044, - host: "localhost" - } - }, + internal: { + clsi: { + port: 3013, + load_port: 3044, + host: 'localhost', + }, + }, - apis: { - clsi: { - url: "http://localhost:3013" - } - }, + apis: { + clsi: { + url: 'http://localhost:3013', + }, + }, - smokeTest: false, - project_cache_length_ms: 1000 * 60 * 60 * 24, - parallelFileDownloads:1 -}; + smokeTest: false, + project_cache_length_ms: 1000 * 60 * 60 * 24, + parallelFileDownloads: 1, +} From 92053fb05bb867a1bd00a3f517bb6d6003dc118d Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Wed, 19 May 2021 18:47:51 +0100 Subject: [PATCH 674/709] Fix formatting --- test/acceptance/scripts/settings.test.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/acceptance/scripts/settings.test.js b/test/acceptance/scripts/settings.test.js index e960ce8b..f9446c31 100644 --- a/test/acceptance/scripts/settings.test.js +++ b/test/acceptance/scripts/settings.test.js @@ -9,8 +9,8 @@ module.exports = { username: 'clsi', password: null, dialect: 'sqlite', - storage: Path.resolve('db.sqlite'), - }, + storage: Path.resolve('db.sqlite') + } }, path: { @@ -22,7 +22,7 @@ module.exports = { synctexBaseDir() { return '/compile' }, - sandboxedCompilesHostDir: process.env.SANDBOXED_COMPILES_HOST_DIR, + sandboxedCompilesHostDir: process.env.SANDBOXED_COMPILES_HOST_DIR }, clsi: { @@ -35,30 +35,30 @@ module.exports = { env: { PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/', - HOME: '/tmp', + HOME: '/tmp' }, modem: { - socketPath: false, + socketPath: false }, - user: process.env.SIBLING_CONTAINER_USER || '111', - }, + user: process.env.SIBLING_CONTAINER_USER || '111' + } }, internal: { clsi: { port: 3013, load_port: 3044, - host: 'localhost', - }, + host: 'localhost' + } }, apis: { clsi: { - url: 'http://localhost:3013', - }, + url: 'http://localhost:3013' + } }, smokeTest: false, project_cache_length_ms: 1000 * 60 * 60 * 24, - parallelFileDownloads: 1, + parallelFileDownloads: 1 } From 9d06a50dee6c6bd5976cf6dc14e124ed2f7ee880 Mon Sep 17 00:00:00 2001 From: Alf Eaton <alf.eaton@overleaf.com> Date: Wed, 19 May 2021 18:57:35 +0100 Subject: [PATCH 675/709] Update references to .coffee files --- docker-compose-config.yml | 6 +++--- test/acceptance/scripts/full-test.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose-config.yml b/docker-compose-config.yml index afe56bbe..0c141eaa 100644 --- a/docker-compose-config.yml +++ b/docker-compose-config.yml @@ -6,7 +6,7 @@ services: ALLOWED_IMAGES: "quay.io/sharelatex/texlive-full:2017.1" TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: "tex" - SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + SHARELATEX_CONFIG: /app/config/settings.defaults.js DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex @@ -16,13 +16,13 @@ services: - ./cache:/app/cache - ./bin/synctex:/app/bin/synctex - + ci: environment: ALLOWED_IMAGES: ${TEXLIVE_IMAGE} TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 TEXLIVE_IMAGE_USER: "tex" - SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + SHARELATEX_CONFIG: /app/config/settings.defaults.js DOCKER_RUNNER: "true" COMPILES_HOST_DIR: $PWD/compiles SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex diff --git a/test/acceptance/scripts/full-test.sh b/test/acceptance/scripts/full-test.sh index a465bdde..953b4945 100755 --- a/test/acceptance/scripts/full-test.sh +++ b/test/acceptance/scripts/full-test.sh @@ -1,6 +1,6 @@ #!/bin/bash -x -export SHARELATEX_CONFIG=`pwd`/test/acceptance/scripts/settings.test.coffee +export SHARELATEX_CONFIG=`pwd`/test/acceptance/scripts/settings.test.js echo ">> Starting server..." From fb2556217c0f970daef58dbe68cb140fcd70dff3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 May 2021 09:35:58 +0000 Subject: [PATCH 676/709] Bump lodash from 4.17.20 to 4.17.21 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 773d9f43..7d1557c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4534,9 +4534,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.at": { "version": "4.6.0", diff --git a/package.json b/package.json index dac3ba9d..b28645e8 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "fs-extra": "^10.0.0", "heapdump": "^0.3.15", "lockfile": "^1.0.4", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "logger-sharelatex": "^2.2.0", "mysql": "^2.18.1", "p-limit": "^3.1.0", From 4e3b27d692c17da9bb800c3a196b0843f2574c91 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 25 May 2021 10:24:49 +0100 Subject: [PATCH 677/709] [misc] AllowedImageNamesTests: add missing done callbacks --- ...ImageNames.js => AllowedImageNamesTests.js} | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) rename test/acceptance/js/{AllowedImageNames.js => AllowedImageNamesTests.js} (88%) diff --git a/test/acceptance/js/AllowedImageNames.js b/test/acceptance/js/AllowedImageNamesTests.js similarity index 88% rename from test/acceptance/js/AllowedImageNames.js rename to test/acceptance/js/AllowedImageNamesTests.js index 8107273f..06304a80 100644 --- a/test/acceptance/js/AllowedImageNames.js +++ b/test/acceptance/js/AllowedImageNamesTests.js @@ -75,7 +75,7 @@ Hello world beforeEach(function (done) { Client.compile(this.project_id, this.request, done) }) - it('should error out with an invalid imageName', function () { + it('should error out with an invalid imageName', function (done) { Client.syncFromCodeWithImage( this.project_id, 'main.tex', @@ -85,11 +85,12 @@ Hello world (error, body) => { expect(String(error)).to.include('statusCode=400') expect(body).to.equal('invalid image') + done() } ) }) - it('should produce a mapping a valid imageName', function () { + it('should produce a mapping a valid imageName', function (done) { Client.syncFromCodeWithImage( this.project_id, 'main.tex', @@ -103,6 +104,7 @@ Hello world { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } ] }) + done() } ) }) @@ -112,7 +114,7 @@ Hello world beforeEach(function (done) { Client.compile(this.project_id, this.request, done) }) - it('should error out with an invalid imageName', function () { + it('should error out with an invalid imageName', function (done) { Client.syncFromPdfWithImage( this.project_id, 'main.tex', @@ -122,11 +124,12 @@ Hello world (error, body) => { expect(String(error)).to.include('statusCode=400') expect(body).to.equal('invalid image') + done() } ) }) - it('should produce a mapping a valid imageName', function () { + it('should produce a mapping a valid imageName', function (done) { Client.syncFromPdfWithImage( this.project_id, 1, @@ -138,6 +141,7 @@ Hello world expect(result).to.deep.equal({ code: [{ file: 'main.tex', line: 3, column: -1 }] }) + done() } ) }) @@ -147,7 +151,7 @@ Hello world beforeEach(function (done) { Client.compile(this.project_id, this.request, done) }) - it('should error out with an invalid imageName', function () { + it('should error out with an invalid imageName', function (done) { Client.wordcountWithImage( this.project_id, 'main.tex', @@ -155,11 +159,12 @@ Hello world (error, body) => { expect(String(error)).to.include('statusCode=400') expect(body).to.equal('invalid image') + done() } ) }) - it('should produce a texcout a valid imageName', function () { + it('should produce a texcout a valid imageName', function (done) { Client.wordcountWithImage( this.project_id, 'main.tex', @@ -168,6 +173,7 @@ Hello world expect(error).to.not.exist expect(result).to.exist expect(result.texcount).to.exist + done() } ) }) From 4aef3b804393f8d6a81325032e680341f19f7e04 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 31 May 2021 10:20:25 +0200 Subject: [PATCH 678/709] [ContentCacheManager] use PDF.js Xref table instead of stream detection (#242) * make the content cache manager tests configurable * extend stream content in unit tests * [ContentCacheManagerTests] prepare for full object caching * filesystem stream for pdfjs * working?? * cleaning up * handle overflow * [misc] install pdfjs-dist * [misc] move pdfjs code into app/lib/ and scripts/, also use CamelCase * [misc] abstract the file loading and parsing of xRef tables into helper * [misc] pdfjsTests: add snapshot based tests for the Xref table parser * [misc] FSStream: throw proper error and drop commented code * [misc] FSStream: integrate throwing of MissingDataException into getter * [misc] pdfjs: fix eslint errors * [misc] pdfjs: run format_fix * [misc] pdfjs: allocate very small non empty dummy buffers explicitly * [misc] install @overleaf/o-error * [ContentCacheManager] use PDF.js Xref table instead of stream detection Co-Authored-By: Brian Gough <brian.gough@overleaf.com> * [pdfjs] parseXrefTable: handle empty PDF files gracefully Co-authored-by: Brian Gough <brian.gough@overleaf.com> --- app/js/ContentCacheManager.js | 174 ++++---- app/js/OutputCacheManager.js | 57 +-- app/lib/pdfjs/FSPdfManager.js | 43 ++ app/lib/pdfjs/FSStream.js | 138 +++++++ app/lib/pdfjs/parseXrefTable.js | 24 ++ package-lock.json | 21 +- package.json | 4 +- scripts/demo-pdfjs-Xref.js | 12 + test/acceptance/fixtures/minimal.pdf | Bin 0 -> 12313 bytes test/unit/js/ContentCacheManagerTests.js | 371 ++++++------------ ...71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71 | 7 + ...17923e3714927bbf35e63ea88bd93c7a8076cf1fcd | Bin 0 -> 10161 bytes test/unit/lib/pdfjsTests.js | 78 ++++ .../lib/snapshots/asymptote/XrefTable.json | 356 +++++++++++++++++ .../biber_bibliography/XrefTable.json | 128 ++++++ .../lib/snapshots/epstopdf/XrefTable.json | 125 ++++++ test/unit/lib/snapshots/feynmf/XrefTable.json | 115 ++++++ test/unit/lib/snapshots/feynmp/XrefTable.json | 102 +++++ .../lib/snapshots/fontawesome/XrefTable.json | 93 +++++ .../fontawesome_xelatex/XrefTable.json | 126 ++++++ .../lib/snapshots/glossaries/XrefTable.json | 94 +++++ .../unit/lib/snapshots/gnuplot/XrefTable.json | 89 +++++ test/unit/lib/snapshots/hebrew/XrefTable.json | 81 ++++ test/unit/lib/snapshots/knitr/XrefTable.json | 145 +++++++ .../lib/snapshots/knitr_utf8/XrefTable.json | 179 +++++++++ .../snapshots/latex_compiler/XrefTable.json | 132 +++++++ .../lualatex_compiler/XrefTable.json | 74 ++++ .../makeindex-custom-style/XrefTable.json | 107 +++++ .../lib/snapshots/makeindex/XrefTable.json | 90 +++++ test/unit/lib/snapshots/minted/XrefTable.json | 77 ++++ .../multibib_bibliography/XrefTable.json | 133 +++++++ .../lib/snapshots/nomenclature/XrefTable.json | 94 +++++ .../references_in_include/XrefTable.json | 90 +++++ .../simple_bibliography/XrefTable.json | 94 +++++ .../snapshots/subdirectories/XrefTable.json | 108 +++++ .../lib/snapshots/tikz_feynman/XrefTable.json | 145 +++++++ .../snapshots/xelatex_compiler/XrefTable.json | 73 ++++ 37 files changed, 3406 insertions(+), 373 deletions(-) create mode 100644 app/lib/pdfjs/FSPdfManager.js create mode 100644 app/lib/pdfjs/FSStream.js create mode 100644 app/lib/pdfjs/parseXrefTable.js create mode 100644 scripts/demo-pdfjs-Xref.js create mode 100644 test/acceptance/fixtures/minimal.pdf create mode 100644 test/unit/js/snapshots/minimalCompile/chunks/896749b8343851b0dc385f71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71 create mode 100644 test/unit/js/snapshots/minimalCompile/chunks/d7cfc73ad2fba4578a437517923e3714927bbf35e63ea88bd93c7a8076cf1fcd create mode 100644 test/unit/lib/pdfjsTests.js create mode 100644 test/unit/lib/snapshots/asymptote/XrefTable.json create mode 100644 test/unit/lib/snapshots/biber_bibliography/XrefTable.json create mode 100644 test/unit/lib/snapshots/epstopdf/XrefTable.json create mode 100644 test/unit/lib/snapshots/feynmf/XrefTable.json create mode 100644 test/unit/lib/snapshots/feynmp/XrefTable.json create mode 100644 test/unit/lib/snapshots/fontawesome/XrefTable.json create mode 100644 test/unit/lib/snapshots/fontawesome_xelatex/XrefTable.json create mode 100644 test/unit/lib/snapshots/glossaries/XrefTable.json create mode 100644 test/unit/lib/snapshots/gnuplot/XrefTable.json create mode 100644 test/unit/lib/snapshots/hebrew/XrefTable.json create mode 100644 test/unit/lib/snapshots/knitr/XrefTable.json create mode 100644 test/unit/lib/snapshots/knitr_utf8/XrefTable.json create mode 100644 test/unit/lib/snapshots/latex_compiler/XrefTable.json create mode 100644 test/unit/lib/snapshots/lualatex_compiler/XrefTable.json create mode 100644 test/unit/lib/snapshots/makeindex-custom-style/XrefTable.json create mode 100644 test/unit/lib/snapshots/makeindex/XrefTable.json create mode 100644 test/unit/lib/snapshots/minted/XrefTable.json create mode 100644 test/unit/lib/snapshots/multibib_bibliography/XrefTable.json create mode 100644 test/unit/lib/snapshots/nomenclature/XrefTable.json create mode 100644 test/unit/lib/snapshots/references_in_include/XrefTable.json create mode 100644 test/unit/lib/snapshots/simple_bibliography/XrefTable.json create mode 100644 test/unit/lib/snapshots/subdirectories/XrefTable.json create mode 100644 test/unit/lib/snapshots/tikz_feynman/XrefTable.json create mode 100644 test/unit/lib/snapshots/xelatex_compiler/XrefTable.json diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index d4246816..ab73c61d 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -7,45 +7,97 @@ const fs = require('fs') const crypto = require('crypto') const Path = require('path') const Settings = require('settings-sharelatex') +const OError = require('@overleaf/o-error') const pLimit = require('p-limit') - -const MIN_CHUNK_SIZE = Settings.pdfCachingMinChunkSize - -const START_OF_STREAM_MARKER = 'stream' -const END_OF_STREAM_MARKER = 'endstream' -const START_OF_STREAM_MARKER_LENGTH = START_OF_STREAM_MARKER.length -const END_OF_STREAM_MARKER_LENGTH = END_OF_STREAM_MARKER.length +const { parseXrefTable } = require('../lib/pdfjs/parseXrefTable') /** * * @param {String} contentDir path to directory where content hash files are cached * @param {String} filePath the pdf file to scan for streams + * @param {number} size the pdf size */ -async function update(contentDir, filePath) { - const stream = fs.createReadStream(filePath) - const extractor = new PdfStreamsExtractor() +async function update(contentDir, filePath, size) { const ranges = [] const newRanges = [] // keep track of hashes expire old ones when they reach a generation > N. const tracker = await HashFileTracker.from(contentDir) tracker.updateAge() - for await (const chunk of stream) { - const pdfStreams = extractor.consume(chunk) - for (const pdfStream of pdfStreams) { - if (pdfStream.end - pdfStream.start < MIN_CHUNK_SIZE) continue - const hash = pdfStreamHash(pdfStream.buffers) + const rawTable = await parseXrefTable(filePath, size) + rawTable.sort((a, b) => { + return a.offset - b.offset + }) + rawTable.forEach((obj, idx) => { + obj.idx = idx + }) + + const uncompressedObjects = [] + for (const object of rawTable) { + if (!object.uncompressed) { + continue + } + const nextObject = rawTable[object.idx + 1] + if (!nextObject) { + // Ignore this possible edge case. + // The last object should be part of the xRef table. + continue + } else { + object.endOffset = nextObject.offset + } + const size = object.endOffset - object.offset + object.size = size + if (size < Settings.pdfCachingMinChunkSize) { + continue + } + uncompressedObjects.push(object) + } - const range = { start: pdfStream.start, end: pdfStream.end, hash } + const handle = await fs.promises.open(filePath) + try { + for (const object of uncompressedObjects) { + let buffer = Buffer.alloc(object.size, 0) + const { bytesRead } = await handle.read( + buffer, + 0, + object.size, + object.offset + ) + if (bytesRead !== object.size) { + throw new OError('could not read full chunk', { + object, + bytesRead + }) + } + const idxObj = buffer.indexOf('obj') + if (idxObj > 100) { + throw new OError('objectId is too large', { + object, + idxObj + }) + } + const objectIdRaw = buffer.subarray(0, idxObj) + buffer = buffer.subarray(objectIdRaw.byteLength) + + const hash = pdfStreamHash(buffer) + const range = { + objectId: objectIdRaw.toString(), + start: object.offset + objectIdRaw.byteLength, + end: object.endOffset, + hash + } ranges.push(range) // Optimization: Skip writing of duplicate streams. if (tracker.track(range)) continue - await writePdfStream(contentDir, hash, pdfStream.buffers) + await writePdfStream(contentDir, hash, buffer) newRanges.push(range) } + } finally { + await handle.close() } + const reclaimedSpace = await tracker.deleteStaleHashes(5) await tracker.flush() return [ranges, newRanges, reclaimedSpace] @@ -142,94 +194,21 @@ class HashFileTracker { } } -class PdfStreamsExtractor { - constructor() { - this.fileIndex = 0 - this.inStream = false - this.streamStartIndex = 0 - this.buffers = [] - this.lastChunk = Buffer.alloc(0) - } - - consume(chunk) { - let chunkIndex = 0 - const pdfStreams = [] - chunk = Buffer.concat([this.lastChunk, chunk]) - while (true) { - if (!this.inStream) { - // Not in a stream, look for stream start - const index = chunk.indexOf(START_OF_STREAM_MARKER, chunkIndex) - if (index === -1) { - // Couldn't find stream start - break - } - // Found stream start, start a stream - this.inStream = true - this.streamStartIndex = this.fileIndex + index - chunkIndex = index - } else { - // In a stream, look for stream end - const index = chunk.indexOf(END_OF_STREAM_MARKER, chunkIndex) - if (index === -1) { - break - } - // add "endstream" part - const endIndex = index + END_OF_STREAM_MARKER_LENGTH - this.buffers.push(chunk.slice(chunkIndex, endIndex)) - pdfStreams.push({ - start: this.streamStartIndex, - end: this.fileIndex + endIndex, - buffers: this.buffers - }) - this.inStream = false - this.buffers = [] - chunkIndex = endIndex - } - } - - const remaining = chunk.length - chunkIndex - const nextMarkerLength = this.inStream - ? END_OF_STREAM_MARKER_LENGTH - : START_OF_STREAM_MARKER_LENGTH - if (remaining > nextMarkerLength) { - const retainMarkerSection = chunk.length - nextMarkerLength - if (this.inStream) { - this.buffers.push(chunk.slice(chunkIndex, retainMarkerSection)) - } - this.lastChunk = chunk.slice(retainMarkerSection) - this.fileIndex += retainMarkerSection - } else { - this.lastChunk = chunk.slice(chunkIndex) - this.fileIndex += chunkIndex - } - return pdfStreams - } -} - -function pdfStreamHash(buffers) { +function pdfStreamHash(buffer) { const hash = crypto.createHash('sha256') - for (const buffer of buffers) { - hash.update(buffer) - } + hash.update(buffer) return hash.digest('hex') } -async function writePdfStream(dir, hash, buffers) { +async function writePdfStream(dir, hash, buffer) { const filename = Path.join(dir, hash) const atomicWriteFilename = filename + '~' - const file = await fs.promises.open(atomicWriteFilename, 'w') if (Settings.enablePdfCachingDark) { // Write an empty file in dark mode. - buffers = [] + buffer = Buffer.alloc(0) } try { - try { - for (const buffer of buffers) { - await file.write(buffer) - } - } finally { - await file.close() - } + await fs.promises.writeFile(atomicWriteFilename, buffer) await fs.promises.rename(atomicWriteFilename, filename) } catch (err) { try { @@ -247,5 +226,8 @@ function promiseMapWithLimit(concurrency, array, fn) { module.exports = { HASH_REGEX: /^[0-9a-f]{64}$/, - update: callbackify(update) + update: callbackify(update), + promises: { + update + } } diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index e6167570..4d2e6413 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -275,36 +275,39 @@ module.exports = OutputCacheManager = { outputDir, OutputCacheManager.path(outputFile.build, outputFile.path) ) + const pdfSize = outputFile.size const timer = new Metrics.Timer('compute-pdf-ranges') - ContentCacheManager.update(contentDir, outputFilePath, function ( - err, - result - ) { - if (err) return callback(err, outputFiles) - const [contentRanges, newContentRanges, reclaimedSpace] = result + ContentCacheManager.update( + contentDir, + outputFilePath, + pdfSize, + function (err, result) { + if (err) return callback(err, outputFiles) + const [contentRanges, newContentRanges, reclaimedSpace] = result - if (Settings.enablePdfCachingDark) { - // In dark mode we are doing the computation only and do not emit - // any ranges to the frontend. - } else { - outputFile.contentId = Path.basename(contentDir) - outputFile.ranges = contentRanges - } + if (Settings.enablePdfCachingDark) { + // In dark mode we are doing the computation only and do not emit + // any ranges to the frontend. + } else { + outputFile.contentId = Path.basename(contentDir) + outputFile.ranges = contentRanges + } - timings['compute-pdf-caching'] = timer.done() - stats['pdf-caching-n-ranges'] = contentRanges.length - stats['pdf-caching-total-ranges-size'] = contentRanges.reduce( - (sum, next) => sum + (next.end - next.start), - 0 - ) - stats['pdf-caching-n-new-ranges'] = newContentRanges.length - stats['pdf-caching-new-ranges-size'] = newContentRanges.reduce( - (sum, next) => sum + (next.end - next.start), - 0 - ) - stats['pdf-caching-reclaimed-space'] = reclaimedSpace - callback(null, outputFiles) - }) + timings['compute-pdf-caching'] = timer.done() + stats['pdf-caching-n-ranges'] = contentRanges.length + stats['pdf-caching-total-ranges-size'] = contentRanges.reduce( + (sum, next) => sum + (next.end - next.start), + 0 + ) + stats['pdf-caching-n-new-ranges'] = newContentRanges.length + stats['pdf-caching-new-ranges-size'] = newContentRanges.reduce( + (sum, next) => sum + (next.end - next.start), + 0 + ) + stats['pdf-caching-reclaimed-space'] = reclaimedSpace + callback(null, outputFiles) + } + ) } else { callback(null, outputFiles) } diff --git a/app/lib/pdfjs/FSPdfManager.js b/app/lib/pdfjs/FSPdfManager.js new file mode 100644 index 00000000..a0450c1e --- /dev/null +++ b/app/lib/pdfjs/FSPdfManager.js @@ -0,0 +1,43 @@ +const { PDFDocument } = require('pdfjs-dist/lib/core/document') +const { LocalPdfManager } = require('pdfjs-dist/lib/core/pdf_manager') +const { MissingDataException } = require('pdfjs-dist/lib/core/core_utils') +const { FSStream } = require('./FSStream') + +class FSPdfManager extends LocalPdfManager { + constructor(docId, options) { + const nonEmptyDummyBuffer = Buffer.alloc(1, 0) + super(docId, nonEmptyDummyBuffer) + this.stream = new FSStream(options.fh, 0, options.size) + this.pdfDocument = new PDFDocument(this, this.stream) + } + + async ensure(obj, prop, args) { + try { + const value = obj[prop] + if (typeof value === 'function') { + return value.apply(obj, args) + } + return value + } catch (ex) { + if (!(ex instanceof MissingDataException)) { + throw ex + } + await this.requestRange(ex.begin, ex.end) + return this.ensure(obj, prop, args) + } + } + + requestRange(begin, end) { + return this.stream.requestRange(begin, end) + } + + requestLoadedStream() {} + + onLoadedStream() {} + + terminate(reason) {} +} + +module.exports = { + FSPdfManager +} diff --git a/app/lib/pdfjs/FSStream.js b/app/lib/pdfjs/FSStream.js new file mode 100644 index 00000000..9179e83f --- /dev/null +++ b/app/lib/pdfjs/FSStream.js @@ -0,0 +1,138 @@ +const { Stream } = require('pdfjs-dist/lib/core/stream') +const { MissingDataException } = require('pdfjs-dist/lib/core/core_utils') + +const BUF_SIZE = 1024 // read from the file in 1024 byte pages + +class FSStream extends Stream { + constructor(fh, start, length, dict, cachedBytes) { + const nonEmptyDummyBuffer = Buffer.alloc(1, 0) + super(nonEmptyDummyBuffer, start, length, dict) + delete this.bytes + this.fh = fh + this.cachedBytes = cachedBytes || [] + } + + get length() { + return this.end - this.start + } + + get isEmpty() { + return this.length === 0 + } + + // Manage cached reads from the file + + requestRange(begin, end) { + // expand small ranges to read a larger amount + if (end - begin < BUF_SIZE) { + end = begin + BUF_SIZE + } + end = Math.min(end, this.length) + // keep a cache of previous reads with {begin,end,buffer} values + const result = { + begin: begin, + end: end, + buffer: Buffer.alloc(end - begin, 0) + } + this.cachedBytes.push(result) + return this.fh.read(result.buffer, 0, end - begin, begin) + } + + _ensureGetPos(pos) { + const found = this.cachedBytes.find((x) => { + return x.begin <= pos && pos < x.end + }) + if (!found) { + throw new MissingDataException(pos, pos + 1) + } + return found + } + + _ensureGetRange(begin, end) { + end = Math.min(end, this.length) // BG: handle overflow case + const found = this.cachedBytes.find((x) => { + return x.begin <= begin && end <= x.end + }) + if (!found) { + throw new MissingDataException(begin, end) + } + return found + } + + _readByte(found, pos) { + return found.buffer[pos - found.begin] + } + + _readBytes(found, pos, end) { + return found.buffer.subarray(pos - found.begin, end - found.begin) + } + + // handle accesses to the bytes + + ensureByte(pos) { + this._ensureGetPos(pos) // may throw a MissingDataException + } + + getByte() { + const pos = this.pos + if (this.pos >= this.end) { + return -1 + } + const found = this._ensureGetPos(pos) + return this._readByte(found, this.pos++) + } + + // BG: for a range, end is not included (see Buffer.subarray for example) + + ensureBytes(length, forceClamped = false) { + const pos = this.pos + this._ensureGetRange(pos, pos + length) + } + + getBytes(length, forceClamped = false) { + const pos = this.pos + const strEnd = this.end + + const found = this._ensureGetRange(pos, pos + length) + if (!length) { + const subarray = this._readBytes(found, pos, strEnd) + // `this.bytes` is always a `Uint8Array` here. + return forceClamped ? new Uint8ClampedArray(subarray) : subarray + } + let end = pos + length + if (end > strEnd) { + end = strEnd + } + this.pos = end + const subarray = this._readBytes(found, pos, end) + // `this.bytes` is always a `Uint8Array` here. + return forceClamped ? new Uint8ClampedArray(subarray) : subarray + } + + getByteRange() { + // BG: this isn't needed as far as I can tell + throw new Error('not implemented') + } + + reset() { + this.pos = this.start + } + + moveStart() { + this.start = this.pos + } + + makeSubStream(start, length, dict = null) { + // BG: had to add this check for null length, it is being called with only + // the start value at one point in the xref decoding. The intent is clear + // enough + // - a null length means "to the end of the file" -- not sure how it is + // working in the existing pdfjs code without this. + if (!length) { + length = this.end - start + } + return new FSStream(this.fh, start, length, dict, this.cachedBytes) + } +} + +module.exports = { FSStream } diff --git a/app/lib/pdfjs/parseXrefTable.js b/app/lib/pdfjs/parseXrefTable.js new file mode 100644 index 00000000..4db1e0cf --- /dev/null +++ b/app/lib/pdfjs/parseXrefTable.js @@ -0,0 +1,24 @@ +const fs = require('fs') +const { FSPdfManager } = require('./FSPdfManager') + +async function parseXrefTable(path, size) { + if (size === 0) { + return [] + } + + const file = await fs.promises.open(path) + try { + const manager = new FSPdfManager(0, { fh: file, size }) + + await manager.ensureDoc('checkHeader') + await manager.ensureDoc('parseStartXRef') + await manager.ensureDoc('parse') + return manager.pdfDocument.catalog.xref.entries + } finally { + file.close() + } +} + +module.exports = { + parseXrefTable +} diff --git a/package-lock.json b/package-lock.json index 773d9f43..52571c6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1008,9 +1008,19 @@ } }, "@overleaf/o-error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-3.0.0.tgz", - "integrity": "sha512-LsM2s6Iy9G97ktPo0ys4VxtI/m3ahc1ZHwjo5XnhXtjeIkkkVAehsrcRRoV/yWepPjymB0oZonhcfojpjYR/tg==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-3.3.1.tgz", + "integrity": "sha512-1FRBYZO0lbJ0U+FRGZVS8ou6RhEw3e2B86WW/NbtBw554g0h5iC8ESf+juIfPMU/WDf/JDIFbg3eB/LnP2RSow==", + "requires": { + "core-js": "^3.8.3" + }, + "dependencies": { + "core-js": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.13.0.tgz", + "integrity": "sha512-iWDbiyha1M5vFwPFmQnvRv+tJzGbFAm6XimJUT0NgHYW3xZEs1SkCAcasWSVFxpI2Xb/V1DDJckq3v90+bQnog==" + } + } }, "@protobufjs/aspromise": { "version": "1.1.2", @@ -5594,6 +5604,11 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "pdfjs-dist": { + "version": "2.7.570", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.7.570.tgz", + "integrity": "sha512-/ZkA1FwkEOyDaq11JhMLazdwQAA0F9uwrP7h/1L9Akt9KWh1G5/tkzS+bPuUELq2s2GDFnaT+kooN/aSjT7DXQ==" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", diff --git a/package.json b/package.json index dac3ba9d..51870b02 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js test/unit/lib", "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", "lint": "node_modules/.bin/eslint --max-warnings 0 .", @@ -20,6 +20,7 @@ "author": "James Allen <james@sharelatex.com>", "dependencies": { "@overleaf/metrics": "^3.5.1", + "@overleaf/o-error": "^3.3.1", "async": "3.2.0", "body-parser": "^1.19.0", "diskusage": "^1.1.3", @@ -32,6 +33,7 @@ "logger-sharelatex": "^2.2.0", "mysql": "^2.18.1", "p-limit": "^3.1.0", + "pdfjs-dist": "^2.7.570", "request": "^2.88.2", "sequelize": "^5.21.5", "settings-sharelatex": "^1.1.0", diff --git a/scripts/demo-pdfjs-Xref.js b/scripts/demo-pdfjs-Xref.js new file mode 100644 index 00000000..86be134e --- /dev/null +++ b/scripts/demo-pdfjs-Xref.js @@ -0,0 +1,12 @@ +const fs = require('fs') +const { parseXrefTable } = require('../app/lib/pdfjs/parseXrefTable') + +const pdfPath = process.argv[2] + +async function main() { + const size = (await fs.promises.stat(pdfPath)).size + const xRefEntries = await parseXrefTable(pdfPath, size) + console.log('Xref entries', xRefEntries) +} + +main().catch(console.error) diff --git a/test/acceptance/fixtures/minimal.pdf b/test/acceptance/fixtures/minimal.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d578e90567e15149674581235b0d67a2f9340924 GIT binary patch literal 12313 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 QA+Q2D5hy7olq3=U7fM=w*#H0l literal 0 HcmV?d00001 diff --git a/test/unit/js/ContentCacheManagerTests.js b/test/unit/js/ContentCacheManagerTests.js index 35f22fff..76c517d8 100644 --- a/test/unit/js/ContentCacheManagerTests.js +++ b/test/unit/js/ContentCacheManagerTests.js @@ -1,201 +1,123 @@ +const fs = require('fs') const Path = require('path') -const crypto = require('crypto') -const { Readable } = require('stream') -const SandboxedModule = require('sandboxed-module') -const sinon = require('sinon') const { expect } = require('chai') const MODULE_PATH = '../../../app/js/ContentCacheManager' -class FakeFile { - constructor() { - this.closed = false - this.contents = [] - } - - async write(blob) { - this.contents.push(blob) - return this - } - - async close() { - this.closed = true - return this - } - - toJSON() { - return { - contents: Buffer.concat(this.contents).toString(), - closed: this.closed - } - } -} - -function hash(blob) { - const hash = crypto.createHash('sha256') - hash.update(blob) - return hash.digest('hex') -} - describe('ContentCacheManager', function () { let contentDir, pdfPath - let ContentCacheManager, fs, files, Settings - function load() { - ContentCacheManager = SandboxedModule.require(MODULE_PATH, { - requires: { - fs, - 'settings-sharelatex': Settings - } - }) - } + let ContentCacheManager, files, Settings + before(function () { + Settings = require('settings-sharelatex') + ContentCacheManager = require(MODULE_PATH) + }) let contentRanges, newContentRanges, reclaimed - function run(filePath, done) { - ContentCacheManager.update(contentDir, filePath, (err, ranges) => { - if (err) return done(err) - let newlyReclaimed - ;[contentRanges, newContentRanges, newlyReclaimed] = ranges - reclaimed += newlyReclaimed - done() - }) + async function run(filePath, size) { + const result = await ContentCacheManager.promises.update( + contentDir, + filePath, + size + ) + let newlyReclaimed + ;[contentRanges, newContentRanges, newlyReclaimed] = result + reclaimed += newlyReclaimed + + const fileNames = await fs.promises.readdir(contentDir) + files = {} + for (const fileName of fileNames) { + const path = Path.join(contentDir, fileName) + files[path] = await fs.promises.readFile(path) + } } - - beforeEach(function () { - reclaimed = 0 + before(function () { contentDir = '/app/output/602cee6f6460fca0ba7921e6/content/1797a7f48f9-5abc1998509dea1f' pdfPath = '/app/output/602cee6f6460fca0ba7921e6/generated-files/1797a7f48ea-8ac6805139f43351/output.pdf' - Settings = { - pdfCachingMinChunkSize: 1024, - enablePdfCachingDark: false - } - files = {} - fs = { - createReadStream: sinon.stub().returns(Readable.from([])), - promises: { - async writeFile(name, blob) { - const file = new FakeFile() - await file.write(Buffer.from(blob)) - await file.close() - files[name] = file - }, - async readFile(name) { - if (!files[name]) { - throw new Error() - } - return files[name].toJSON().contents - }, - async open(name) { - files[name] = new FakeFile() - return files[name] - }, - async stat(name) { - if (!files[name]) { - throw new Error() - } - }, - async rename(oldName, newName) { - if (!files[oldName]) { - throw new Error() - } - files[newName] = files[oldName] - delete files[oldName] - }, - async unlink(name) { - if (!files[name]) { - throw new Error() - } - delete files[name] - } - } - } + + reclaimed = 0 + Settings.pdfCachingMinChunkSize = 1024 + }) + + before(async function () { + await fs.promises.rmdir(contentDir, { recursive: true }) + await fs.promises.mkdir(contentDir, { recursive: true }) + await fs.promises.mkdir(Path.dirname(pdfPath), { recursive: true }) }) - describe('with a small minChunkSize', function () { - beforeEach(function () { - Settings.pdfCachingMinChunkSize = 1 - load() + describe('minimal', function () { + const PATH_MINIMAL = 'test/acceptance/fixtures/minimal.pdf' + const OBJECT_ID_1 = '9 0 ' + const HASH_LARGE = + 'd7cfc73ad2fba4578a437517923e3714927bbf35e63ea88bd93c7a8076cf1fcd' + const OBJECT_ID_2 = '10 0 ' + const HASH_SMALL = + '896749b8343851b0dc385f71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71' + function getChunkPath(hash) { + return Path.join('test/unit/js/snapshots/minimalCompile/chunks', hash) + } + let MINIMAL_SIZE, RANGE_1, RANGE_2, h1, h2, START_1, START_2, END_1, END_2 + before(async function () { + await fs.promises.copyFile(PATH_MINIMAL, pdfPath) + const MINIMAL = await fs.promises.readFile(PATH_MINIMAL) + MINIMAL_SIZE = (await fs.promises.stat(PATH_MINIMAL)).size + RANGE_1 = await fs.promises.readFile(getChunkPath(HASH_LARGE)) + RANGE_2 = await fs.promises.readFile(getChunkPath(HASH_SMALL)) + h1 = HASH_LARGE + h2 = HASH_SMALL + START_1 = MINIMAL.indexOf(RANGE_1) + END_1 = START_1 + RANGE_1.byteLength + START_2 = MINIMAL.indexOf(RANGE_2) + END_2 = START_2 + RANGE_2.byteLength }) + async function runWithMinimal() { + await run(pdfPath, MINIMAL_SIZE) + } - describe('when the ranges are split across chunks', function () { - const RANGE_1 = 'stream123endstream' - const RANGE_2 = 'stream(||)endstream' - const RANGE_3 = 'stream!$%/=endstream' - const h1 = hash(RANGE_1) - const h2 = hash(RANGE_2) - const h3 = hash(RANGE_3) - function runWithSplitStream(done) { - fs.createReadStream - .withArgs(pdfPath) - .returns( - Readable.from([ - Buffer.from('abcstr'), - Buffer.from('eam123endstreamABC'), - Buffer.from('str'), - Buffer.from('eam(||'), - Buffer.from(')end'), - Buffer.from('stream-_~stream!$%/=endstream') - ]) - ) - run(pdfPath, done) - } - beforeEach(function (done) { - runWithSplitStream(done) + describe('with two ranges qualifying', function () { + before(function () { + Settings.pdfCachingMinChunkSize = 500 }) - - it('should produce three ranges', function () { - expect(contentRanges).to.have.length(3) + before(async function () { + await runWithMinimal() + }) + it('should produce two ranges', function () { + expect(contentRanges).to.have.length(2) }) it('should find the correct offsets', function () { expect(contentRanges).to.deep.equal([ { - start: 3, - end: 21, - hash: hash(RANGE_1) - }, - { - start: 24, - end: 43, - hash: hash(RANGE_2) + objectId: OBJECT_ID_1, + start: START_1, + end: END_1, + hash: h1 }, { - start: 46, - end: 66, - hash: hash(RANGE_3) + objectId: OBJECT_ID_2, + start: START_2, + end: END_2, + hash: h2 } ]) }) it('should store the contents', function () { - expect(JSON.parse(JSON.stringify(files))).to.deep.equal({ - [Path.join(contentDir, h1)]: { - contents: RANGE_1, - closed: true - }, - [Path.join(contentDir, h2)]: { - contents: RANGE_2, - closed: true - }, - [Path.join(contentDir, h3)]: { - contents: RANGE_3, - closed: true - }, - [Path.join(contentDir, '.state.v0.json')]: { - contents: JSON.stringify({ + expect(files).to.deep.equal({ + [Path.join(contentDir, h1)]: RANGE_1, + [Path.join(contentDir, h2)]: RANGE_2, + [Path.join(contentDir, '.state.v0.json')]: Buffer.from( + JSON.stringify({ hashAge: [ [h1, 0], - [h2, 0], - [h3, 0] + [h2, 0] ], hashSize: [ - [h1, 18], - [h2, 19], - [h3, 20] + [h1, RANGE_1.byteLength], + [h2, RANGE_2.byteLength] ] - }), - closed: true - } + }) + ) }) }) @@ -203,71 +125,46 @@ describe('ContentCacheManager', function () { expect(contentRanges).to.deep.equal(newContentRanges) }) - describe('when re-running with one stream removed', function () { - function runWithOneSplitStreamRemoved(done) { - fs.createReadStream - .withArgs(pdfPath) - .returns( - Readable.from([ - Buffer.from('abcstr'), - Buffer.from('eam123endstreamABC'), - Buffer.from('stream!$%/=endstream') - ]) - ) - run(pdfPath, done) - } - beforeEach(function (done) { - runWithOneSplitStreamRemoved(done) + describe('when re-running with one range too small', function () { + before(function () { + Settings.pdfCachingMinChunkSize = 1024 + }) + + before(async function () { + await runWithMinimal() }) - it('should produce two ranges', function () { - expect(contentRanges).to.have.length(2) + it('should produce one range', function () { + expect(contentRanges).to.have.length(1) }) it('should find the correct offsets', function () { expect(contentRanges).to.deep.equal([ { - start: 3, - end: 21, - hash: hash(RANGE_1) - }, - { - start: 24, - end: 44, - hash: hash(RANGE_3) + objectId: OBJECT_ID_1, + start: START_1, + end: END_1, + hash: h1 } ]) }) it('should update the age of the 2nd range', function () { - expect(JSON.parse(JSON.stringify(files))).to.deep.equal({ - [Path.join(contentDir, h1)]: { - contents: RANGE_1, - closed: true - }, - [Path.join(contentDir, h2)]: { - contents: RANGE_2, - closed: true - }, - [Path.join(contentDir, h3)]: { - contents: RANGE_3, - closed: true - }, - [Path.join(contentDir, '.state.v0.json')]: { - contents: JSON.stringify({ + expect(files).to.deep.equal({ + [Path.join(contentDir, h1)]: RANGE_1, + [Path.join(contentDir, h2)]: RANGE_2, + [Path.join(contentDir, '.state.v0.json')]: Buffer.from( + JSON.stringify({ hashAge: [ [h1, 0], - [h2, 1], - [h3, 0] + [h2, 1] ], hashSize: [ - [h1, 18], - [h2, 19], - [h3, 20] + [h1, RANGE_1.byteLength], + [h2, RANGE_2.byteLength] ] - }), - closed: true - } + }) + ) }) }) @@ -277,53 +174,35 @@ describe('ContentCacheManager', function () { describe('when re-running 5 more times', function () { for (let i = 0; i < 5; i++) { - beforeEach(function (done) { - runWithOneSplitStreamRemoved(done) + before(async function () { + await runWithMinimal() }) } - it('should still produce two ranges', function () { - expect(contentRanges).to.have.length(2) + it('should still produce one range', function () { + expect(contentRanges).to.have.length(1) }) it('should still find the correct offsets', function () { expect(contentRanges).to.deep.equal([ { - start: 3, - end: 21, - hash: hash(RANGE_1) - }, - { - start: 24, - end: 44, - hash: hash(RANGE_3) + objectId: OBJECT_ID_1, + start: START_1, + end: END_1, + hash: h1 } ]) }) it('should delete the 2nd range', function () { - expect(JSON.parse(JSON.stringify(files))).to.deep.equal({ - [Path.join(contentDir, h1)]: { - contents: RANGE_1, - closed: true - }, - [Path.join(contentDir, h3)]: { - contents: RANGE_3, - closed: true - }, - [Path.join(contentDir, '.state.v0.json')]: { - contents: JSON.stringify({ - hashAge: [ - [h1, 0], - [h3, 0] - ], - hashSize: [ - [h1, 18], - [h3, 20] - ] - }), - closed: true - } + expect(files).to.deep.equal({ + [Path.join(contentDir, h1)]: RANGE_1, + [Path.join(contentDir, '.state.v0.json')]: Buffer.from( + JSON.stringify({ + hashAge: [[h1, 0]], + hashSize: [[h1, RANGE_1.byteLength]] + }) + ) }) }) @@ -332,7 +211,7 @@ describe('ContentCacheManager', function () { }) it('should yield the reclaimed space', function () { - expect(reclaimed).to.equal(RANGE_2.length) + expect(reclaimed).to.equal(RANGE_2.byteLength) }) }) }) diff --git a/test/unit/js/snapshots/minimalCompile/chunks/896749b8343851b0dc385f71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71 b/test/unit/js/snapshots/minimalCompile/chunks/896749b8343851b0dc385f71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71 new file mode 100644 index 00000000..bb9f891b --- /dev/null +++ b/test/unit/js/snapshots/minimalCompile/chunks/896749b8343851b0dc385f71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71 @@ -0,0 +1,7 @@ +obj +<< /Type /ObjStm /Length 447 /Filter /FlateDecode /N 5 /First 32 >> +stream +xRQk0~߯ǍK'ɒ $vkGIJ[(y8*$IRd>I"H@9@J!` V/gg f>BZxJ9ۮ]-B'ZNg k%i\!f4m݁49ĶCY]\@! 4c Uf=JgOg>zz>A)C9WwKqPÜ#/48VX/ Tp -%2"*B;X2,9Gz;EΥJj/c +n%ᵦf3]!=y ,s]@e+COW.Ckڒ c_ťX v>N2u7=} #HVr9?kv6G^z׬.v=Uyjǡpz2 +endstream +endobj diff --git a/test/unit/js/snapshots/minimalCompile/chunks/d7cfc73ad2fba4578a437517923e3714927bbf35e63ea88bd93c7a8076cf1fcd b/test/unit/js/snapshots/minimalCompile/chunks/d7cfc73ad2fba4578a437517923e3714927bbf35e63ea88bd93c7a8076cf1fcd new file mode 100644 index 0000000000000000000000000000000000000000..d503090d3a7389ff13f6c9a2106c66e0ecfbd73b GIT binary patch literal 10161 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 literal 0 HcmV?d00001 diff --git a/test/unit/lib/pdfjsTests.js b/test/unit/lib/pdfjsTests.js new file mode 100644 index 00000000..64f062c0 --- /dev/null +++ b/test/unit/lib/pdfjsTests.js @@ -0,0 +1,78 @@ +const fs = require('fs') +const Path = require('path') +const { expect } = require('chai') +const { parseXrefTable } = require('../../../app/lib/pdfjs/parseXrefTable') +const PATH_EXAMPLES = 'test/acceptance/fixtures/examples/' +const PATH_SNAPSHOTS = 'test/unit/lib/snapshots/' +const EXAMPLES = fs.readdirSync(PATH_EXAMPLES) + +function snapshotPath(example) { + return Path.join(PATH_SNAPSHOTS, example, 'XrefTable.json') +} + +function pdfPath(example) { + return Path.join(PATH_EXAMPLES, example, 'output.pdf') +} + +async function loadContext(example) { + const size = (await fs.promises.stat(pdfPath(example))).size + + let blob + try { + blob = await fs.promises.readFile(snapshotPath(example)) + } catch (e) { + if (e.code !== 'ENOENT') { + throw e + } + } + const snapshot = blob ? JSON.parse(blob) : null + return { + size, + snapshot + } +} + +async function backFillSnapshot(example, size) { + const table = await parseXrefTable(pdfPath(example), size) + await fs.promises.mkdir(Path.dirname(snapshotPath(example)), { + recursive: true + }) + await fs.promises.writeFile( + snapshotPath(example), + JSON.stringify(table, null, 2) + ) + return table +} + +describe('pdfjs', function () { + describe('when the pdf is an empty file', function () { + it('should yield no entries', async function () { + const path = 'does/not/matter.pdf' + const table = await parseXrefTable(path, 0) + expect(table).to.deep.equal([]) + }) + }) + + for (const example of EXAMPLES) { + describe(example, function () { + let size, snapshot + before('load snapshot', async function () { + const ctx = await loadContext(example) + size = ctx.size + snapshot = ctx.snapshot + }) + + before('back fill new snapshot', async function () { + if (snapshot === null) { + console.error('back filling snapshot for', example) + snapshot = await backFillSnapshot(example, size) + } + }) + + it('should produce the expected xRef table', async function () { + const table = await parseXrefTable(pdfPath(example), size) + expect(table).to.deep.equal(snapshot) + }) + }) + } +}) diff --git a/test/unit/lib/snapshots/asymptote/XrefTable.json b/test/unit/lib/snapshots/asymptote/XrefTable.json new file mode 100644 index 00000000..be9f2916 --- /dev/null +++ b/test/unit/lib/snapshots/asymptote/XrefTable.json @@ -0,0 +1,356 @@ +[ + { + "offset": 0, + "gen": 0, + "free": true + }, + { + "offset": 123086, + "gen": 0, + "uncompressed": true + }, + { + "offset": 123405, + "gen": 0, + "uncompressed": true + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 216, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1084, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1244, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4001, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4155, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4297, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4932, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5307, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5495, + "gen": 0, + "uncompressed": true + }, + { + "offset": 30246, + "gen": 0, + "uncompressed": true + }, + { + "offset": 31466, + "gen": 0, + "uncompressed": true + }, + { + "offset": 38398, + "gen": 0, + "uncompressed": true + }, + { + "offset": 39039, + "gen": 0, + "uncompressed": true + }, + { + "offset": 40158, + "gen": 0, + "uncompressed": true + }, + { + "offset": 40897, + "gen": 0, + "uncompressed": true + }, + { + "offset": 65550, + "gen": 0, + "uncompressed": true + }, + { + "offset": 74691, + "gen": 0, + "uncompressed": true + }, + { + "offset": 81693, + "gen": 0, + "uncompressed": true + }, + { + "offset": 97169, + "gen": 0, + "uncompressed": true + }, + { + "offset": 104103, + "gen": 0, + "uncompressed": true + }, + { + "offset": 111180, + "gen": 0, + "uncompressed": true + }, + { + "offset": 118555, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 0 + }, + { + "offset": 6, + "gen": 1 + }, + { + "offset": 6, + "gen": 2 + }, + { + "offset": 6, + "gen": 3 + }, + { + "offset": 6, + "gen": 4 + }, + { + "offset": 6, + "gen": 5 + }, + { + "offset": 6, + "gen": 6 + }, + { + "offset": 6, + "gen": 7 + }, + { + "offset": 6, + "gen": 8 + }, + { + "offset": 6, + "gen": 9 + }, + { + "offset": 6, + "gen": 10 + }, + { + "offset": 6, + "gen": 11 + }, + { + "offset": 6, + "gen": 12 + }, + { + "offset": 6, + "gen": 13 + }, + { + "offset": 6, + "gen": 14 + }, + { + "offset": 6, + "gen": 15 + }, + { + "offset": 6, + "gen": 16 + }, + { + "offset": 6, + "gen": 17 + }, + { + "offset": 6, + "gen": 18 + }, + { + "offset": 6, + "gen": 19 + }, + { + "offset": 6, + "gen": 20 + }, + { + "offset": 6, + "gen": 21 + }, + { + "offset": 6, + "gen": 22 + }, + { + "offset": 6, + "gen": 23 + }, + { + "offset": 6, + "gen": 24 + }, + { + "offset": 6, + "gen": 25 + }, + { + "offset": 6, + "gen": 26 + }, + { + "offset": 6, + "gen": 27 + }, + { + "offset": 6, + "gen": 28 + }, + { + "offset": 6, + "gen": 29 + }, + { + "offset": 6, + "gen": 30 + }, + { + "offset": 6, + "gen": 31 + }, + { + "offset": 6, + "gen": 32 + }, + { + "offset": 6, + "gen": 33 + }, + { + "offset": 6, + "gen": 34 + }, + { + "offset": 6, + "gen": 35 + }, + { + "offset": 6, + "gen": 36 + }, + { + "offset": 6, + "gen": 37 + }, + { + "offset": 6, + "gen": 38 + }, + { + "offset": 6, + "gen": 39 + }, + { + "offset": 6, + "gen": 40 + }, + { + "offset": 6, + "gen": 41 + }, + { + "offset": 6, + "gen": 42 + }, + { + "offset": 6, + "gen": 43 + }, + { + "offset": 6, + "gen": 44 + }, + { + "offset": 6, + "gen": 45 + }, + { + "offset": 6, + "gen": 46 + }, + { + "offset": 6, + "gen": 47 + }, + { + "offset": 6, + "gen": 48 + }, + { + "offset": 6, + "gen": 49 + }, + { + "offset": 6, + "gen": 50 + }, + { + "offset": 6, + "gen": 51 + }, + { + "offset": 6, + "gen": 52 + }, + { + "offset": 6, + "gen": 53 + }, + { + "offset": 6, + "gen": 54 + }, + { + "offset": 6, + "gen": 55 + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/biber_bibliography/XrefTable.json b/test/unit/lib/snapshots/biber_bibliography/XrefTable.json new file mode 100644 index 00000000..94c0fddf --- /dev/null +++ b/test/unit/lib/snapshots/biber_bibliography/XrefTable.json @@ -0,0 +1,128 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 9, + "gen": 1 + }, + { + "offset": 9, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 14 + }, + { + "offset": 9, + "gen": 12 + }, + { + "offset": 9, + "gen": 15 + }, + { + "offset": 9, + "gen": 13 + }, + { + "offset": 9, + "gen": 16 + }, + { + "offset": 57274, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 17 + }, + { + "offset": 9, + "gen": 2 + }, + { + "offset": 9, + "gen": 3 + }, + { + "offset": 9, + "gen": 4 + }, + { + "offset": 9, + "gen": 5 + }, + { + "offset": 9, + "gen": 6 + }, + { + "offset": 522, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 7 + }, + { + "offset": 8788, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 8 + }, + { + "offset": 17289, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 9 + }, + { + "offset": 32619, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 10 + }, + { + "offset": 44596, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 11 + }, + { + "offset": 9, + "gen": 18 + }, + { + "offset": 57027, + "gen": 0, + "uncompressed": true + }, + { + "offset": 58655, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/epstopdf/XrefTable.json b/test/unit/lib/snapshots/epstopdf/XrefTable.json new file mode 100644 index 00000000..fca6e544 --- /dev/null +++ b/test/unit/lib/snapshots/epstopdf/XrefTable.json @@ -0,0 +1,125 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 208, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 11 + }, + { + "offset": 6, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 14 + }, + { + "offset": 29580, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 15 + }, + { + "offset": 6, + "gen": 1 + }, + { + "offset": 6, + "gen": 2 + }, + { + "offset": 6, + "gen": 3 + }, + { + "offset": 6, + "gen": 4 + }, + { + "offset": 6, + "gen": 5 + }, + { + "offset": 6, + "gen": 6 + }, + { + "offset": 6, + "gen": 7 + }, + { + "offset": 6, + "gen": 8 + }, + { + "offset": 18310, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 9 + }, + { + "offset": 6, + "gen": 10 + }, + { + "offset": 18554, + "gen": 0, + "uncompressed": true + }, + { + "offset": 18945, + "gen": 0, + "uncompressed": true + }, + { + "offset": 19674, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 12 + }, + { + "offset": 22318, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 13 + }, + { + "offset": 6, + "gen": 16 + }, + { + "offset": 29321, + "gen": 0, + "uncompressed": true + }, + { + "offset": 30697, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/feynmf/XrefTable.json b/test/unit/lib/snapshots/feynmf/XrefTable.json new file mode 100644 index 00000000..0da87fc0 --- /dev/null +++ b/test/unit/lib/snapshots/feynmf/XrefTable.json @@ -0,0 +1,115 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 8, + "gen": 1 + }, + { + "offset": 8, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 5 + }, + { + "offset": 8, + "gen": 12 + }, + { + "offset": 8, + "gen": 14 + }, + { + "offset": 8, + "gen": 13 + }, + { + "offset": 25628, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 15 + }, + { + "offset": 8, + "gen": 2 + }, + { + "offset": 8, + "gen": 3 + }, + { + "offset": 8, + "gen": 4 + }, + { + "offset": 250, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 6 + }, + { + "offset": 8, + "gen": 7 + }, + { + "offset": 8, + "gen": 8 + }, + { + "offset": 3854, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 9 + }, + { + "offset": 11227, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 10 + }, + { + "offset": 18230, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 11 + }, + { + "offset": 8, + "gen": 16 + }, + { + "offset": 25381, + "gen": 0, + "uncompressed": true + }, + { + "offset": 26423, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/feynmp/XrefTable.json b/test/unit/lib/snapshots/feynmp/XrefTable.json new file mode 100644 index 00000000..34048b03 --- /dev/null +++ b/test/unit/lib/snapshots/feynmp/XrefTable.json @@ -0,0 +1,102 @@ +[ + { + "offset": 0, + "gen": 65535, + "free": true + }, + { + "offset": 1094, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5512, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1035, + "gen": 0, + "uncompressed": true + }, + { + "offset": 875, + "gen": 0, + "uncompressed": true + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 856, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1159, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1593, + "gen": 0, + "uncompressed": true + }, + { + "offset": 3149, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1436, + "gen": 0, + "uncompressed": true + }, + { + "offset": 2412, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1282, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1768, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1200, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1230, + "gen": 0, + "uncompressed": true + }, + { + "offset": 2006, + "gen": 0, + "uncompressed": true + }, + { + "offset": 2625, + "gen": 0, + "uncompressed": true + }, + { + "offset": 3381, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4069, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/fontawesome/XrefTable.json b/test/unit/lib/snapshots/fontawesome/XrefTable.json new file mode 100644 index 00000000..1951e17f --- /dev/null +++ b/test/unit/lib/snapshots/fontawesome/XrefTable.json @@ -0,0 +1,93 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 7, + "gen": 1 + }, + { + "offset": 7, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 11 + }, + { + "offset": 7, + "gen": 10 + }, + { + "offset": 7, + "gen": 9 + }, + { + "offset": 29476, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 12 + }, + { + "offset": 7, + "gen": 7 + }, + { + "offset": 7, + "gen": 2 + }, + { + "offset": 7, + "gen": 8 + }, + { + "offset": 7, + "gen": 3 + }, + { + "offset": 7, + "gen": 4 + }, + { + "offset": 255, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 5 + }, + { + "offset": 17910, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 6 + }, + { + "offset": 7, + "gen": 13 + }, + { + "offset": 29228, + "gen": 0, + "uncompressed": true + }, + { + "offset": 30448, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/fontawesome_xelatex/XrefTable.json b/test/unit/lib/snapshots/fontawesome_xelatex/XrefTable.json new file mode 100644 index 00000000..e6315a28 --- /dev/null +++ b/test/unit/lib/snapshots/fontawesome_xelatex/XrefTable.json @@ -0,0 +1,126 @@ +[ + { + "offset": 0, + "gen": 0, + "free": true + }, + { + "offset": 6338, + "gen": 0, + "uncompressed": true + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 216, + "gen": 0, + "uncompressed": true + }, + { + "offset": 707, + "gen": 0, + "uncompressed": true + }, + { + "offset": 757, + "gen": 0, + "uncompressed": true + }, + { + "offset": 888, + "gen": 0, + "uncompressed": true + }, + { + "offset": 991, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1257, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1678, + "gen": 0, + "uncompressed": true + }, + { + "offset": 2050, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4246, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4339, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5382, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5475, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5513, + "gen": 0, + "uncompressed": true + }, + { + "offset": 15, + "gen": 0 + }, + { + "offset": 15, + "gen": 1 + }, + { + "offset": 15, + "gen": 2 + }, + { + "offset": 15, + "gen": 3 + }, + { + "offset": 15, + "gen": 4 + }, + { + "offset": 15, + "gen": 5 + }, + { + "offset": 15, + "gen": 6 + }, + { + "offset": 15, + "gen": 7 + }, + { + "offset": 15, + "gen": 8 + }, + { + "offset": 15, + "gen": 9 + }, + { + "offset": 15, + "gen": 10 + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/glossaries/XrefTable.json b/test/unit/lib/snapshots/glossaries/XrefTable.json new file mode 100644 index 00000000..51f7dca8 --- /dev/null +++ b/test/unit/lib/snapshots/glossaries/XrefTable.json @@ -0,0 +1,94 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 7, + "gen": 1 + }, + { + "offset": 7, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 10 + }, + { + "offset": 7, + "gen": 9 + }, + { + "offset": 7, + "gen": 8 + }, + { + "offset": 33085, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 11 + }, + { + "offset": 7, + "gen": 2 + }, + { + "offset": 7, + "gen": 3 + }, + { + "offset": 7, + "gen": 4 + }, + { + "offset": 445, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 5 + }, + { + "offset": 10048, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 6 + }, + { + "offset": 18151, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 7 + }, + { + "offset": 7, + "gen": 12 + }, + { + "offset": 32838, + "gen": 0, + "uncompressed": true + }, + { + "offset": 34157, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/gnuplot/XrefTable.json b/test/unit/lib/snapshots/gnuplot/XrefTable.json new file mode 100644 index 00000000..51480596 --- /dev/null +++ b/test/unit/lib/snapshots/gnuplot/XrefTable.json @@ -0,0 +1,89 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 4, + "gen": 0 + }, + { + "offset": 4, + "gen": 1 + }, + { + "offset": 4, + "gen": 2 + }, + { + "offset": 22047, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4, + "gen": 4 + }, + { + "offset": 4, + "gen": 3 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4, + "gen": 10 + }, + { + "offset": 4, + "gen": 9 + }, + { + "offset": 4, + "gen": 11 + }, + { + "offset": 4, + "gen": 5 + }, + { + "offset": 4, + "gen": 6 + }, + { + "offset": 6451, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4, + "gen": 7 + }, + { + "offset": 14825, + "gen": 0, + "uncompressed": true + }, + { + "offset": 4, + "gen": 8 + }, + { + "offset": 4, + "gen": 12 + }, + { + "offset": 21800, + "gen": 0, + "uncompressed": true + }, + { + "offset": 22696, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/hebrew/XrefTable.json b/test/unit/lib/snapshots/hebrew/XrefTable.json new file mode 100644 index 00000000..d3e8221a --- /dev/null +++ b/test/unit/lib/snapshots/hebrew/XrefTable.json @@ -0,0 +1,81 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 6, + "gen": 1 + }, + { + "offset": 6, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 8 + }, + { + "offset": 6, + "gen": 7 + }, + { + "offset": 22744, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 9 + }, + { + "offset": 6, + "gen": 2 + }, + { + "offset": 6, + "gen": 6 + }, + { + "offset": 6, + "gen": 3 + }, + { + "offset": 364, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 4 + }, + { + "offset": 12163, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 5 + }, + { + "offset": 6, + "gen": 10 + }, + { + "offset": 22496, + "gen": 0, + "uncompressed": true + }, + { + "offset": 23856, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/knitr/XrefTable.json b/test/unit/lib/snapshots/knitr/XrefTable.json new file mode 100644 index 00000000..d57e1247 --- /dev/null +++ b/test/unit/lib/snapshots/knitr/XrefTable.json @@ -0,0 +1,145 @@ +[ + { + "offset": 0, + "gen": 0, + "free": true + }, + { + "offset": 43543, + "gen": 0, + "uncompressed": true + }, + { + "offset": 43792, + "gen": 0, + "uncompressed": true + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 216, + "gen": 0, + "uncompressed": true + }, + { + "offset": 734, + "gen": 0, + "uncompressed": true + }, + { + "offset": 784, + "gen": 0, + "uncompressed": true + }, + { + "offset": 912, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1020, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1544, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5791, + "gen": 0, + "uncompressed": true + }, + { + "offset": 12911, + "gen": 0, + "uncompressed": true + }, + { + "offset": 23655, + "gen": 0, + "uncompressed": true + }, + { + "offset": 30651, + "gen": 0, + "uncompressed": true + }, + { + "offset": 42597, + "gen": 0, + "uncompressed": true + }, + { + "offset": 14, + "gen": 0 + }, + { + "offset": 14, + "gen": 1 + }, + { + "offset": 14, + "gen": 2 + }, + { + "offset": 14, + "gen": 3 + }, + { + "offset": 14, + "gen": 4 + }, + { + "offset": 14, + "gen": 5 + }, + { + "offset": 14, + "gen": 6 + }, + { + "offset": 14, + "gen": 7 + }, + { + "offset": 14, + "gen": 8 + }, + { + "offset": 14, + "gen": 9 + }, + { + "offset": 14, + "gen": 10 + }, + { + "offset": 14, + "gen": 11 + }, + { + "offset": 14, + "gen": 12 + }, + { + "offset": 14, + "gen": 13 + }, + { + "offset": 14, + "gen": 14 + }, + { + "offset": 14, + "gen": 15 + }, + { + "offset": 14, + "gen": 16 + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/knitr_utf8/XrefTable.json b/test/unit/lib/snapshots/knitr_utf8/XrefTable.json new file mode 100644 index 00000000..14982555 --- /dev/null +++ b/test/unit/lib/snapshots/knitr_utf8/XrefTable.json @@ -0,0 +1,179 @@ +[ + { + "offset": 0, + "gen": 0, + "free": true + }, + { + "offset": 75291, + "gen": 0, + "uncompressed": true + }, + { + "offset": 75540, + "gen": 0, + "uncompressed": true + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 216, + "gen": 0, + "uncompressed": true + }, + { + "offset": 790, + "gen": 0, + "uncompressed": true + }, + { + "offset": 840, + "gen": 0, + "uncompressed": true + }, + { + "offset": 975, + "gen": 0, + "uncompressed": true + }, + { + "offset": 1083, + "gen": 0, + "uncompressed": true + }, + { + "offset": 2127, + "gen": 0, + "uncompressed": true + }, + { + "offset": 13797, + "gen": 0, + "uncompressed": true + }, + { + "offset": 23679, + "gen": 0, + "uncompressed": true + }, + { + "offset": 31863, + "gen": 0, + "uncompressed": true + }, + { + "offset": 36111, + "gen": 0, + "uncompressed": true + }, + { + "offset": 50346, + "gen": 0, + "uncompressed": true + }, + { + "offset": 61562, + "gen": 0, + "uncompressed": true + }, + { + "offset": 73508, + "gen": 0, + "uncompressed": true + }, + { + "offset": 16, + "gen": 0 + }, + { + "offset": 16, + "gen": 1 + }, + { + "offset": 16, + "gen": 2 + }, + { + "offset": 16, + "gen": 3 + }, + { + "offset": 16, + "gen": 4 + }, + { + "offset": 16, + "gen": 5 + }, + { + "offset": 16, + "gen": 6 + }, + { + "offset": 16, + "gen": 7 + }, + { + "offset": 16, + "gen": 8 + }, + { + "offset": 16, + "gen": 9 + }, + { + "offset": 16, + "gen": 10 + }, + { + "offset": 16, + "gen": 11 + }, + { + "offset": 16, + "gen": 12 + }, + { + "offset": 16, + "gen": 13 + }, + { + "offset": 16, + "gen": 14 + }, + { + "offset": 16, + "gen": 15 + }, + { + "offset": 16, + "gen": 16 + }, + { + "offset": 16, + "gen": 17 + }, + { + "offset": 16, + "gen": 18 + }, + { + "offset": 16, + "gen": 19 + }, + { + "offset": 16, + "gen": 20 + }, + { + "offset": 16, + "gen": 21 + }, + { + "offset": 16, + "gen": 22 + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/latex_compiler/XrefTable.json b/test/unit/lib/snapshots/latex_compiler/XrefTable.json new file mode 100644 index 00000000..91e596e5 --- /dev/null +++ b/test/unit/lib/snapshots/latex_compiler/XrefTable.json @@ -0,0 +1,132 @@ +[ + { + "offset": 0, + "gen": 65535, + "free": true + }, + { + "offset": 17349, + "gen": 0, + "uncompressed": true + }, + { + "offset": 25448, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17290, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17130, + "gen": 0, + "uncompressed": true + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17109, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17414, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17548, + "gen": 0, + "uncompressed": true + }, + { + "offset": 18912, + "gen": 0, + "uncompressed": true + }, + { + "offset": 18381, + "gen": 0, + "uncompressed": true + }, + { + "offset": 22406, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17897, + "gen": 0, + "uncompressed": true + }, + { + "offset": 21796, + "gen": 0, + "uncompressed": true + }, + { + "offset": 18758, + "gen": 0, + "uncompressed": true + }, + { + "offset": 23361, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17455, + "gen": 0, + "uncompressed": true + }, + { + "offset": 17485, + "gen": 0, + "uncompressed": true + }, + { + "offset": 19218, + "gen": 0, + "uncompressed": true + }, + { + "offset": 22021, + "gen": 0, + "uncompressed": true + }, + { + "offset": 22638, + "gen": 0, + "uncompressed": true + }, + { + "offset": 23599, + "gen": 0, + "uncompressed": true + }, + { + "offset": 18051, + "gen": 0, + "uncompressed": true + }, + { + "offset": 18142, + "gen": 0, + "uncompressed": true + }, + { + "offset": 18657, + "gen": 0, + "uncompressed": true + }, + { + "offset": 24005, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/lualatex_compiler/XrefTable.json b/test/unit/lib/snapshots/lualatex_compiler/XrefTable.json new file mode 100644 index 00000000..a403bb4f --- /dev/null +++ b/test/unit/lib/snapshots/lualatex_compiler/XrefTable.json @@ -0,0 +1,74 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 6, + "gen": 1 + }, + { + "offset": 6, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 4 + }, + { + "offset": 6, + "gen": 6 + }, + { + "offset": 2320, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 2 + }, + { + "offset": 202, + "gen": 0, + "uncompressed": true + }, + { + "offset": 298, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 3 + }, + { + "offset": 1642, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 5 + }, + { + "offset": 6, + "gen": 7 + }, + { + "offset": 2120, + "gen": 0, + "uncompressed": true + }, + { + "offset": 2892, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/makeindex-custom-style/XrefTable.json b/test/unit/lib/snapshots/makeindex-custom-style/XrefTable.json new file mode 100644 index 00000000..b2b9dcef --- /dev/null +++ b/test/unit/lib/snapshots/makeindex-custom-style/XrefTable.json @@ -0,0 +1,107 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 5, + "gen": 1 + }, + { + "offset": 5, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 12 + }, + { + "offset": 30470, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 13 + }, + { + "offset": 5, + "gen": 3 + }, + { + "offset": 5, + "gen": 2 + }, + { + "offset": 366, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 11 + }, + { + "offset": 5, + "gen": 10 + }, + { + "offset": 5, + "gen": 4 + }, + { + "offset": 5, + "gen": 5 + }, + { + "offset": 5, + "gen": 6 + }, + { + "offset": 638, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 7 + }, + { + "offset": 7838, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 8 + }, + { + "offset": 15674, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 9 + }, + { + "offset": 5, + "gen": 14 + }, + { + "offset": 30223, + "gen": 0, + "uncompressed": true + }, + { + "offset": 31457, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/makeindex/XrefTable.json b/test/unit/lib/snapshots/makeindex/XrefTable.json new file mode 100644 index 00000000..9a58bed4 --- /dev/null +++ b/test/unit/lib/snapshots/makeindex/XrefTable.json @@ -0,0 +1,90 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 5, + "gen": 1 + }, + { + "offset": 5, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 9 + }, + { + "offset": 23352, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 10 + }, + { + "offset": 5, + "gen": 3 + }, + { + "offset": 5, + "gen": 2 + }, + { + "offset": 366, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 8 + }, + { + "offset": 5, + "gen": 4 + }, + { + "offset": 5, + "gen": 5 + }, + { + "offset": 589, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 6 + }, + { + "offset": 8425, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 7 + }, + { + "offset": 5, + "gen": 11 + }, + { + "offset": 23105, + "gen": 0, + "uncompressed": true + }, + { + "offset": 24245, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/minted/XrefTable.json b/test/unit/lib/snapshots/minted/XrefTable.json new file mode 100644 index 00000000..f0d2f2a1 --- /dev/null +++ b/test/unit/lib/snapshots/minted/XrefTable.json @@ -0,0 +1,77 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 6, + "gen": 1 + }, + { + "offset": 6, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 7 + }, + { + "offset": 6, + "gen": 6 + }, + { + "offset": 19462, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 8 + }, + { + "offset": 6, + "gen": 2 + }, + { + "offset": 6, + "gen": 3 + }, + { + "offset": 340, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 4 + }, + { + "offset": 7343, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 5 + }, + { + "offset": 6, + "gen": 9 + }, + { + "offset": 19215, + "gen": 0, + "uncompressed": true + }, + { + "offset": 20089, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/multibib_bibliography/XrefTable.json b/test/unit/lib/snapshots/multibib_bibliography/XrefTable.json new file mode 100644 index 00000000..ffd44df5 --- /dev/null +++ b/test/unit/lib/snapshots/multibib_bibliography/XrefTable.json @@ -0,0 +1,133 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 6, + "gen": 1 + }, + { + "offset": 6, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 14 + }, + { + "offset": 6, + "gen": 15 + }, + { + "offset": 40591, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 17 + }, + { + "offset": 6, + "gen": 3 + }, + { + "offset": 6, + "gen": 2 + }, + { + "offset": 290, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 16 + }, + { + "offset": 6, + "gen": 5 + }, + { + "offset": 6, + "gen": 4 + }, + { + "offset": 595, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 7 + }, + { + "offset": 6, + "gen": 6 + }, + { + "offset": 844, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 8 + }, + { + "offset": 6, + "gen": 9 + }, + { + "offset": 6, + "gen": 10 + }, + { + "offset": 1107, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 11 + }, + { + "offset": 11816, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 12 + }, + { + "offset": 28180, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6, + "gen": 13 + }, + { + "offset": 6, + "gen": 18 + }, + { + "offset": 40344, + "gen": 0, + "uncompressed": true + }, + { + "offset": 41851, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/nomenclature/XrefTable.json b/test/unit/lib/snapshots/nomenclature/XrefTable.json new file mode 100644 index 00000000..51286af2 --- /dev/null +++ b/test/unit/lib/snapshots/nomenclature/XrefTable.json @@ -0,0 +1,94 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 7, + "gen": 1 + }, + { + "offset": 7, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 8 + }, + { + "offset": 7, + "gen": 9 + }, + { + "offset": 7, + "gen": 10 + }, + { + "offset": 32363, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 11 + }, + { + "offset": 7, + "gen": 2 + }, + { + "offset": 7, + "gen": 3 + }, + { + "offset": 7, + "gen": 4 + }, + { + "offset": 565, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 5 + }, + { + "offset": 10031, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 6 + }, + { + "offset": 18203, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 7 + }, + { + "offset": 7, + "gen": 12 + }, + { + "offset": 32116, + "gen": 0, + "uncompressed": true + }, + { + "offset": 33492, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/references_in_include/XrefTable.json b/test/unit/lib/snapshots/references_in_include/XrefTable.json new file mode 100644 index 00000000..b0c67312 --- /dev/null +++ b/test/unit/lib/snapshots/references_in_include/XrefTable.json @@ -0,0 +1,90 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 5, + "gen": 1 + }, + { + "offset": 5, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 9 + }, + { + "offset": 15855, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 10 + }, + { + "offset": 5, + "gen": 3 + }, + { + "offset": 5, + "gen": 2 + }, + { + "offset": 167, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 8 + }, + { + "offset": 5, + "gen": 4 + }, + { + "offset": 5, + "gen": 5 + }, + { + "offset": 341, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 6 + }, + { + "offset": 7911, + "gen": 0, + "uncompressed": true + }, + { + "offset": 5, + "gen": 7 + }, + { + "offset": 5, + "gen": 11 + }, + { + "offset": 15608, + "gen": 0, + "uncompressed": true + }, + { + "offset": 16597, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/simple_bibliography/XrefTable.json b/test/unit/lib/snapshots/simple_bibliography/XrefTable.json new file mode 100644 index 00000000..d633b7ba --- /dev/null +++ b/test/unit/lib/snapshots/simple_bibliography/XrefTable.json @@ -0,0 +1,94 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 7, + "gen": 1 + }, + { + "offset": 7, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 9 + }, + { + "offset": 7, + "gen": 8 + }, + { + "offset": 7, + "gen": 10 + }, + { + "offset": 35573, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 11 + }, + { + "offset": 7, + "gen": 2 + }, + { + "offset": 7, + "gen": 3 + }, + { + "offset": 7, + "gen": 4 + }, + { + "offset": 373, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 5 + }, + { + "offset": 8639, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 6 + }, + { + "offset": 23349, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7, + "gen": 7 + }, + { + "offset": 7, + "gen": 12 + }, + { + "offset": 35326, + "gen": 0, + "uncompressed": true + }, + { + "offset": 36668, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/subdirectories/XrefTable.json b/test/unit/lib/snapshots/subdirectories/XrefTable.json new file mode 100644 index 00000000..06ed0fdf --- /dev/null +++ b/test/unit/lib/snapshots/subdirectories/XrefTable.json @@ -0,0 +1,108 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 548, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 1 + }, + { + "offset": 9, + "gen": 2 + }, + { + "offset": 9, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 10 + }, + { + "offset": 9, + "gen": 9 + }, + { + "offset": 9, + "gen": 11 + }, + { + "offset": 46398, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 12 + }, + { + "offset": 8100, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 3 + }, + { + "offset": 9, + "gen": 4 + }, + { + "offset": 9, + "gen": 5 + }, + { + "offset": 9693, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 6 + }, + { + "offset": 17959, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 7 + }, + { + "offset": 34174, + "gen": 0, + "uncompressed": true + }, + { + "offset": 9, + "gen": 8 + }, + { + "offset": 9, + "gen": 13 + }, + { + "offset": 46151, + "gen": 0, + "uncompressed": true + }, + { + "offset": 47562, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/tikz_feynman/XrefTable.json b/test/unit/lib/snapshots/tikz_feynman/XrefTable.json new file mode 100644 index 00000000..afeaa845 --- /dev/null +++ b/test/unit/lib/snapshots/tikz_feynman/XrefTable.json @@ -0,0 +1,145 @@ +[ + { + "offset": 0, + "gen": 255, + "free": true + }, + { + "offset": 8, + "gen": 8 + }, + { + "offset": 8, + "gen": 9 + }, + { + "offset": 8, + "gen": 10 + }, + { + "offset": 8, + "gen": 1 + }, + { + "offset": 8, + "gen": 0 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 20 + }, + { + "offset": 33577, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 3 + }, + { + "offset": 8, + "gen": 2 + }, + { + "offset": 1151, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 17 + }, + { + "offset": 8, + "gen": 19 + }, + { + "offset": 8, + "gen": 18 + }, + { + "offset": 8, + "gen": 5 + }, + { + "offset": 8, + "gen": 4 + }, + { + "offset": 2721, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 7 + }, + { + "offset": 8, + "gen": 6 + }, + { + "offset": 5757, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 11 + }, + { + "offset": 8, + "gen": 12 + }, + { + "offset": 8, + "gen": 13 + }, + { + "offset": 9558, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 14 + }, + { + "offset": 18967, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 15 + }, + { + "offset": 26388, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 16 + }, + { + "offset": 8, + "gen": 21 + }, + { + "offset": 33354, + "gen": 0, + "uncompressed": true + }, + { + "offset": 34451, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file diff --git a/test/unit/lib/snapshots/xelatex_compiler/XrefTable.json b/test/unit/lib/snapshots/xelatex_compiler/XrefTable.json new file mode 100644 index 00000000..31080439 --- /dev/null +++ b/test/unit/lib/snapshots/xelatex_compiler/XrefTable.json @@ -0,0 +1,73 @@ +[ + { + "offset": 0, + "gen": 65535, + "free": true + }, + { + "offset": 8, + "gen": 4 + }, + { + "offset": 8, + "gen": 3 + }, + { + "offset": 8, + "gen": 1 + }, + { + "offset": 8, + "gen": 6 + }, + { + "offset": 8, + "gen": 8 + }, + { + "offset": 15, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 0 + }, + { + "offset": 6857, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 2 + }, + { + "offset": 265, + "gen": 0, + "uncompressed": true + }, + { + "offset": 8, + "gen": 7 + }, + { + "offset": 8, + "gen": 5 + }, + { + "offset": 701, + "gen": 0, + "uncompressed": true + }, + { + "offset": 6750, + "gen": 0, + "uncompressed": true + }, + { + "offset": 7655, + "gen": 0, + "uncompressed": true + } +] \ No newline at end of file From 650c33c96b6ad642ef0def93a2712f35788d465b Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 1 Jun 2021 15:51:09 +0100 Subject: [PATCH 679/709] [misc] wait for refreshing of expiry timeout before triggering cleanup --- app.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 77ab9229..8dd8a8cb 100644 --- a/app.js +++ b/app.js @@ -412,8 +412,9 @@ if (!module.parent) { module.exports = app setInterval(() => { - ProjectPersistenceManager.refreshExpiryTimeout() - ProjectPersistenceManager.clearExpiredProjects() + ProjectPersistenceManager.refreshExpiryTimeout(() => { + ProjectPersistenceManager.clearExpiredProjects() + }) }, tenMinutes) function __guard__(value, transform) { From 93f8706cba4baa7a8c775562a5a5f137a3adc68e Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 1 Jun 2021 15:52:41 +0100 Subject: [PATCH 680/709] [ProjectPersistenceManager] check all user content dirs for full disk --- app/js/ProjectPersistenceManager.js | 41 ++++++++++++------- .../unit/js/ProjectPersistenceManagerTests.js | 18 ++++---- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index d0fd4c24..ca903f44 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -21,34 +21,45 @@ const logger = require('logger-sharelatex') const oneDay = 24 * 60 * 60 * 1000 const Settings = require('settings-sharelatex') const diskusage = require('diskusage') +const { callbackify } = require('util') -module.exports = ProjectPersistenceManager = { - EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5, - - refreshExpiryTimeout(callback) { - if (callback == null) { - callback = function (error) {} - } - diskusage.check('/', function (err, stats) { - if (err) { - logger.err({ err: err }, 'error getting disk usage') - return callback(err) - } +async function refreshExpiryTimeout() { + const paths = [ + Settings.path.compilesDir, + Settings.path.outputDir, + Settings.path.clsiCacheDir + ] + for (const path of paths) { + try { + const stats = await diskusage.check(path) const lowDisk = stats.available / stats.total < 0.1 + const lowerExpiry = ProjectPersistenceManager.EXPIRY_TIMEOUT * 0.9 if (lowDisk && Settings.project_cache_length_ms / 2 < lowerExpiry) { logger.warn( { - stats: stats, + stats, newExpiryTimeoutInDays: (lowerExpiry / oneDay).toFixed(2) }, 'disk running low on space, modifying EXPIRY_TIMEOUT' ) ProjectPersistenceManager.EXPIRY_TIMEOUT = lowerExpiry + break } - callback() - }) + } catch (err) { + logger.err({ err, path }, 'error getting disk usage') + } + } +} + +module.exports = ProjectPersistenceManager = { + EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5, + + promises: { + refreshExpiryTimeout }, + + refreshExpiryTimeout: callbackify(refreshExpiryTimeout), markProjectAsJustAccessed(project_id, callback) { if (callback == null) { callback = function (error) {} diff --git a/test/unit/js/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js index 5fb18272..e60de54f 100644 --- a/test/unit/js/ProjectPersistenceManagerTests.js +++ b/test/unit/js/ProjectPersistenceManagerTests.js @@ -28,7 +28,12 @@ describe('ProjectPersistenceManager', function () { './CompileManager': (this.CompileManager = {}), diskusage: (this.diskusage = { check: sinon.stub() }), 'settings-sharelatex': (this.settings = { - project_cache_length_ms: 1000 + project_cache_length_ms: 1000, + path: { + compilesDir: '/compiles', + outputDir: '/output', + clsiCacheDir: '/cache' + } }), './db': (this.db = {}) } @@ -40,7 +45,7 @@ describe('ProjectPersistenceManager', function () { describe('refreshExpiryTimeout', function () { it('should leave expiry alone if plenty of disk', function (done) { - this.diskusage.check.callsArgWith(1, null, { + this.diskusage.check.resolves({ available: 40, total: 100 }) @@ -54,7 +59,7 @@ describe('ProjectPersistenceManager', function () { }) it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function (done) { - this.diskusage.check.callsArgWith(1, null, { + this.diskusage.check.resolves({ available: 5, total: 100 }) @@ -66,7 +71,7 @@ describe('ProjectPersistenceManager', function () { }) it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function (done) { - this.diskusage.check.callsArgWith(1, null, { + this.diskusage.check.resolves({ available: 5, total: 100 }) @@ -78,10 +83,7 @@ describe('ProjectPersistenceManager', function () { }) it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', function (done) { - this.diskusage.check.callsArgWith(1, 'Error', { - available: 5, - total: 100 - }) + this.diskusage.check.throws(new Error()) this.ProjectPersistenceManager.refreshExpiryTimeout(() => { this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000) done() From 0d997cd86e6e3d05295a31e84148eccaf847e22e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Jun 2021 07:58:49 +0000 Subject: [PATCH 681/709] Bump glob-parent from 5.1.0 to 5.1.2 Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.0 to 5.1.2. - [Release notes](https://github.com/gulpjs/glob-parent/releases) - [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md) - [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.0...v5.1.2) --- updated-dependencies: - dependency-name: glob-parent dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52571c6c..cca636d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3474,9 +3474,9 @@ } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" From 2c36a9c290f11258e089bedf4007c574c9585f80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Jun 2021 07:23:12 +0000 Subject: [PATCH 682/709] Bump normalize-url from 4.5.0 to 4.5.1 Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 4.5.0 to 4.5.1. - [Release notes](https://github.com/sindresorhus/normalize-url/releases) - [Commits](https://github.com/sindresorhus/normalize-url/commits) --- updated-dependencies: - dependency-name: normalize-url dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52571c6c..3bc09ef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5271,9 +5271,9 @@ "dev": true }, "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, "npm-bundled": { From 6bca519d8313f425d631ac96a6f370a8e304c7a5 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 14 Jun 2021 12:24:48 +0100 Subject: [PATCH 683/709] [misc] track delay of using sqlite for last access time of a project --- app/js/ProjectPersistenceManager.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index d0fd4c24..f5d32caa 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -21,6 +21,7 @@ const logger = require('logger-sharelatex') const oneDay = 24 * 60 * 60 * 1000 const Settings = require('settings-sharelatex') const diskusage = require('diskusage') +const Metrics = require('./Metrics') module.exports = ProjectPersistenceManager = { EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5, @@ -53,6 +54,7 @@ module.exports = ProjectPersistenceManager = { if (callback == null) { callback = function (error) {} } + const timer = new Metrics.Timer('db-bump-last-accessed') const job = (cb) => db.Project.findOrCreate({ where: { project_id } }) .spread((project, created) => @@ -62,7 +64,10 @@ module.exports = ProjectPersistenceManager = { .error(cb) ) .error(cb) - return dbQueue.queue.push(job, callback) + dbQueue.queue.push(job, (error) => { + timer.done() + callback(error) + }) }, clearExpiredProjects(callback) { From 8ebeb556b59b1aa1b3c423637a346a8bc52b17e9 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 14 Jun 2021 13:03:02 +0100 Subject: [PATCH 684/709] [misc] track delays of using sqlite for url caching details --- app/js/UrlCache.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index 23afafaa..a8b2b19b 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -21,6 +21,7 @@ const crypto = require('crypto') const fs = require('fs') const logger = require('logger-sharelatex') const async = require('async') +const Metrics = require('./Metrics') module.exports = UrlCache = { downloadUrlToFile(project_id, url, destPath, lastModified, callback) { @@ -206,17 +207,22 @@ module.exports = UrlCache = { if (callback == null) { callback = function (error, urlDetails) {} } + const timer = new Metrics.Timer('db-find-url-details') const job = (cb) => db.UrlCache.findOne({ where: { url, project_id } }) .then((urlDetails) => cb(null, urlDetails)) .error(cb) - return dbQueue.queue.push(job, callback) + dbQueue.queue.push(job, (error, urlDetails) => { + timer.done() + callback(error, urlDetails) + }) }, _updateOrCreateUrlDetails(project_id, url, lastModified, callback) { if (callback == null) { callback = function (error) {} } + const timer = new Metrics.Timer('db-update-or-create-url-details') const job = (cb) => db.UrlCache.findOrCreate({ where: { url, project_id } }) .spread((urlDetails, created) => @@ -226,24 +232,32 @@ module.exports = UrlCache = { .error(cb) ) .error(cb) - return dbQueue.queue.push(job, callback) + dbQueue.queue.push(job, (error) => { + timer.done() + callback(error) + }) }, _clearUrlDetails(project_id, url, callback) { if (callback == null) { callback = function (error) {} } + const timer = new Metrics.Timer('db-clear-url-details') const job = (cb) => db.UrlCache.destroy({ where: { url, project_id } }) .then(() => cb(null)) .error(cb) - return dbQueue.queue.push(job, callback) + dbQueue.queue.push(job, (error) => { + timer.done() + callback(error) + }) }, _findAllUrlsInProject(project_id, callback) { if (callback == null) { callback = function (error, urls) {} } + const timer = new Metrics.Timer('db-find-urls-in-project') const job = (cb) => db.UrlCache.findAll({ where: { project_id } }) .then((urlEntries) => @@ -253,6 +267,9 @@ module.exports = UrlCache = { ) ) .error(cb) - return dbQueue.queue.push(job, callback) + dbQueue.queue.push(job, (err, urls) => { + timer.done() + callback(err, urls) + }) } } From 7582cbb134b619dc270321fe04400580087399cf Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 15 Jun 2021 09:08:32 +0100 Subject: [PATCH 685/709] [misc] move import to avoid git conflict --- app/js/ProjectPersistenceManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index f5d32caa..d0e3af3a 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -12,6 +12,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let ProjectPersistenceManager +const Metrics = require('./Metrics') const UrlCache = require('./UrlCache') const CompileManager = require('./CompileManager') const db = require('./db') @@ -21,7 +22,6 @@ const logger = require('logger-sharelatex') const oneDay = 24 * 60 * 60 * 1000 const Settings = require('settings-sharelatex') const diskusage = require('diskusage') -const Metrics = require('./Metrics') module.exports = ProjectPersistenceManager = { EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5, From 7b84f7cdac335aed61034e491e99b1aa8b357eb8 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 15 Jun 2021 09:24:23 +0100 Subject: [PATCH 686/709] [misc] add missing dependency entry for send --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 51870b02..3b3e9719 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "p-limit": "^3.1.0", "pdfjs-dist": "^2.7.570", "request": "^2.88.2", + "send": "^0.17.1", "sequelize": "^5.21.5", "settings-sharelatex": "^1.1.0", "sqlite3": "^4.1.1", From 54ae7406263535bb77db4fb2ea050cc2093c09df Mon Sep 17 00:00:00 2001 From: Henry Oswald <henry.oswald@sharelatex.com> Date: Mon, 21 Jun 2021 15:18:51 +0100 Subject: [PATCH 687/709] very basic mvp for shedding load When the box is over 100% capacity start to shed load. Use 5 min load average to make the system less likely to start moving projects off after a temporary cpu spike --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 8dd8a8cb..b2c261bc 100644 --- a/app.js +++ b/app.js @@ -331,7 +331,7 @@ const loadTcpServer = net.createServer(function (socket) { const freeLoad = availableWorkingCpus - currentLoad let freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) if (freeLoadPercentage <= 0) { - freeLoadPercentage = 1 // when its 0 the server is set to drain and will move projects to different servers + freeLoadPercentage = 0 // when its 0 the server is set to drain and will move projects to different servers } socket.write(`up, ${freeLoadPercentage}%\n`, 'ASCII') return socket.end() From bbcb97a74cc637b6e6133bc8346f0ab1104520af Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 22 Jun 2021 12:13:19 +0100 Subject: [PATCH 688/709] [misc] CompileController: simplify composing of outputFiles --- app/js/CompileController.js | 14 ++------------ test/unit/js/CompileControllerTests.js | 12 ++---------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/app/js/CompileController.js b/app/js/CompileController.js index 2813bca9..4307736d 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -123,7 +123,7 @@ module.exports = CompileController = { stats, timings, outputFiles: outputFiles.map((file) => { - const record = { + return { url: `${Settings.apis.clsi.url}/project/${request.project_id}` + (request.user_id != null @@ -131,18 +131,8 @@ module.exports = CompileController = { : '') + (file.build != null ? `/build/${file.build}` : '') + `/output/${file.path}`, - path: file.path, - type: file.type, - build: file.build, - contentId: file.contentId + ...file } - if (file.ranges != null) { - record.ranges = file.ranges - } - if (file.size != null) { - record.size = file.size - } - return record }) } }) diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 6d6b34ee..6236be77 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -157,11 +157,7 @@ describe('CompileController', function () { outputFiles: this.output_files.map((file) => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, - path: file.path, - type: file.type, - build: file.build, - // gets dropped by JSON.stringify - contentId: undefined + ...file } }) } @@ -202,11 +198,7 @@ describe('CompileController', function () { outputFiles: this.output_files.map((file) => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, - path: file.path, - type: file.type, - build: file.build, - // gets dropped by JSON.stringify - contentId: undefined + ...file } }) } From 73d7540444c9d3a3c910bbd58884b3212bbde247 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 22 Jun 2021 12:14:33 +0100 Subject: [PATCH 689/709] [CompileController] emit status=failure for an empty output.pdf file --- app/js/CompileController.js | 2 +- test/unit/js/CompileControllerTests.js | 43 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/app/js/CompileController.js b/app/js/CompileController.js index 4307736d..63a4106a 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -88,7 +88,7 @@ module.exports = CompileController = { let file status = 'failure' for (file of Array.from(outputFiles)) { - if (file.path === 'output.pdf') { + if (file.path === 'output.pdf' && file.size > 0) { status = 'success' } } diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 6236be77..3b29c867 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -99,6 +99,7 @@ describe('CompileController', function () { { path: 'output.pdf', type: 'pdf', + size: 1337, build: 1234 }, { @@ -207,6 +208,48 @@ describe('CompileController', function () { }) }) + describe('with an empty output.pdf', function () { + beforeEach(function () { + this.output_files = [ + { + path: 'output.pdf', + type: 'pdf', + size: 0, + build: 1234 + }, + { + path: 'output.log', + type: 'log', + build: 1234 + } + ] + this.CompileManager.doCompileWithLock = sinon + .stub() + .yields(null, this.output_files, this.stats, this.timings) + this.CompileController.compile(this.req, this.res) + }) + + it('should return the JSON response with status failure', function () { + this.res.status.calledWith(200).should.equal(true) + this.res.send + .calledWith({ + compile: { + status: 'failure', + error: null, + stats: this.stats, + timings: this.timings, + outputFiles: this.output_files.map((file) => { + return { + url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, + ...file + } + }) + } + }) + .should.equal(true) + }) + }) + describe('with an error', function () { beforeEach(function () { this.CompileManager.doCompileWithLock = sinon From f3c4146f80c6c6762b693777316170ade730d858 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 23 Jun 2021 09:15:05 +0100 Subject: [PATCH 690/709] [misc] ContentCacheMetrics: log slow pdf caching performance --- app/js/ContentCacheMetrics.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/js/ContentCacheMetrics.js b/app/js/ContentCacheMetrics.js index f5f9188d..d2da9d86 100644 --- a/app/js/ContentCacheMetrics.js +++ b/app/js/ContentCacheMetrics.js @@ -1,4 +1,20 @@ +const logger = require('logger-sharelatex') const Metrics = require('./Metrics') +const os = require('os') + +let CACHED_LOAD = { + expires: -1, + load: [0, 0, 0] +} +function getSystemLoad() { + if (CACHED_LOAD.expires < Date.now()) { + CACHED_LOAD = { + expires: Date.now() + 10 * 1000, + load: os.loadavg() + } + } + return CACHED_LOAD.load +} const ONE_MB = 1024 * 1024 @@ -20,6 +36,16 @@ function emitPdfCachingStats(stats, timings) { ? timings.compileE2E / (timings.compileE2E - timings['compute-pdf-caching']) : 1 + if (fraction > 1.5) { + logger.warn( + { + stats, + timings, + load: getSystemLoad() + }, + 'slow pdf caching' + ) + } Metrics.summary('overhead-compute-pdf-ranges', fraction * 100 - 100) // How does the hashing scale to pdf size in MB? From c2e72151931b1c9ee8c5959216c10ad5a4065f69 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 23 Jun 2021 11:27:19 +0100 Subject: [PATCH 691/709] [misc] ContentCacheMetrics: apply review feedback: ignore fast compiles Co-Authored-By: Brian Gough <brian.gough@overleaf.com> --- app/js/ContentCacheMetrics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/js/ContentCacheMetrics.js b/app/js/ContentCacheMetrics.js index d2da9d86..5c54ba08 100644 --- a/app/js/ContentCacheMetrics.js +++ b/app/js/ContentCacheMetrics.js @@ -36,7 +36,7 @@ function emitPdfCachingStats(stats, timings) { ? timings.compileE2E / (timings.compileE2E - timings['compute-pdf-caching']) : 1 - if (fraction > 1.5) { + if (fraction > 1.5 && timings.compileE2E > 10 * 1000) { logger.warn( { stats, From 15ea54abd108a45453fd594698de2d5cb00ee437 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 23 Jun 2021 11:28:31 +0100 Subject: [PATCH 692/709] [ContentCacheMetrics] add new metric for absolute time spent in PDF.js --- app/js/ContentCacheMetrics.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/js/ContentCacheMetrics.js b/app/js/ContentCacheMetrics.js index 5c54ba08..00784069 100644 --- a/app/js/ContentCacheMetrics.js +++ b/app/js/ContentCacheMetrics.js @@ -30,6 +30,9 @@ function emitPdfStats(stats, timings) { function emitPdfCachingStats(stats, timings) { if (!stats['pdf-size']) return // double check + // How much extra time did we spent in PDF.js? + Metrics.timing('compute-pdf-caching', timings['compute-pdf-caching']) + // How large is the overhead of hashing up-front? const fraction = timings.compileE2E - timings['compute-pdf-caching'] !== 0 From 6743bac4d7af8560ecec1ce4df8dda7a72893d77 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 23 Jun 2021 14:14:28 +0100 Subject: [PATCH 693/709] [misc] bail out from pdf caching processing after 10s or earlier ...for fast compiles. --- app/js/CompileManager.js | 5 ++-- app/js/ContentCacheManager.js | 44 ++++++++++++++++++++++++++++++--- app/js/ContentCacheMetrics.js | 3 +++ app/js/Errors.js | 5 ++++ app/js/OutputCacheManager.js | 10 ++++++++ app/lib/pdfjs/FSPdfManager.js | 4 +-- app/lib/pdfjs/FSStream.js | 14 +++++++++-- app/lib/pdfjs/parseXrefTable.js | 7 ++++-- config/settings.defaults.js | 4 ++- test/unit/lib/pdfjsTests.js | 22 +++++++++++++++-- 10 files changed, 103 insertions(+), 15 deletions(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index c771082b..1d681e8d 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -292,6 +292,8 @@ module.exports = CompileManager = { timings['cpu-time'] / stats['latex-runs'] ) } + // Emit compile time. + timings.compile = ts return OutputFileFinder.findOutputFiles( resourceList, @@ -317,8 +319,7 @@ module.exports = CompileManager = { ) } - // Emit compile time. - timings.compile = ts + // Emit e2e compile time. timings.compileE2E = timerE2E.done() if (stats['pdf-size']) { diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index ab73c61d..cbd6f3cd 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -10,21 +10,26 @@ const Settings = require('settings-sharelatex') const OError = require('@overleaf/o-error') const pLimit = require('p-limit') const { parseXrefTable } = require('../lib/pdfjs/parseXrefTable') +const { TimedOutError } = require('./Errors') /** * * @param {String} contentDir path to directory where content hash files are cached * @param {String} filePath the pdf file to scan for streams * @param {number} size the pdf size + * @param {number} compileTime */ -async function update(contentDir, filePath, size) { +async function update(contentDir, filePath, size, compileTime) { + const checkDeadline = getDeadlineChecker(compileTime) const ranges = [] const newRanges = [] // keep track of hashes expire old ones when they reach a generation > N. const tracker = await HashFileTracker.from(contentDir) tracker.updateAge() - const rawTable = await parseXrefTable(filePath, size) + checkDeadline('after init HashFileTracker') + + const rawTable = await parseXrefTable(filePath, size, checkDeadline) rawTable.sort((a, b) => { return a.offset - b.offset }) @@ -32,6 +37,8 @@ async function update(contentDir, filePath, size) { obj.idx = idx }) + checkDeadline('after parsing') + const uncompressedObjects = [] for (const object of rawTable) { if (!object.uncompressed) { @@ -50,12 +57,14 @@ async function update(contentDir, filePath, size) { if (size < Settings.pdfCachingMinChunkSize) { continue } - uncompressedObjects.push(object) + uncompressedObjects.push({ object, idx: uncompressedObjects.length }) } + checkDeadline('after finding uncompressed') + const handle = await fs.promises.open(filePath) try { - for (const object of uncompressedObjects) { + for (const { object, idx } of uncompressedObjects) { let buffer = Buffer.alloc(object.size, 0) const { bytesRead } = await handle.read( buffer, @@ -63,6 +72,7 @@ async function update(contentDir, filePath, size) { object.size, object.offset ) + checkDeadline('after read ' + idx) if (bytesRead !== object.size) { throw new OError('could not read full chunk', { object, @@ -80,6 +90,7 @@ async function update(contentDir, filePath, size) { buffer = buffer.subarray(objectIdRaw.byteLength) const hash = pdfStreamHash(buffer) + checkDeadline('after hash ' + idx) const range = { objectId: objectIdRaw.toString(), start: object.offset + objectIdRaw.byteLength, @@ -92,12 +103,15 @@ async function update(contentDir, filePath, size) { if (tracker.track(range)) continue await writePdfStream(contentDir, hash, buffer) + checkDeadline('after write ' + idx) newRanges.push(range) } } finally { await handle.close() } + // NOTE: Bailing out below does not make sense. + // Let the next compile use the already written ranges. const reclaimedSpace = await tracker.deleteStaleHashes(5) await tracker.flush() return [ranges, newRanges, reclaimedSpace] @@ -219,6 +233,28 @@ async function writePdfStream(dir, hash, buffer) { } } +function getDeadlineChecker(compileTime) { + const maxOverhead = Math.min( + // Adding 10s to a 40s compile time is OK. + compileTime / 4, + // Adding 30s to a 120s compile time is not OK, limit to 10s. + Settings.pdfCachingMaxProcessingTime + ) + + const deadline = Date.now() + maxOverhead + let lastStage = { stage: 'start', now: Date.now() } + return function (stage) { + const now = Date.now() + if (now > deadline) { + throw new TimedOutError(stage, { + lastStage: lastStage.stage, + diffToLastStage: now - lastStage.now + }) + } + lastStage = { stage, now } + } +} + function promiseMapWithLimit(concurrency, array, fn) { const limit = pLimit(concurrency) return Promise.all(array.map((x) => limit(() => fn(x)))) diff --git a/app/js/ContentCacheMetrics.js b/app/js/ContentCacheMetrics.js index 00784069..6b7a33de 100644 --- a/app/js/ContentCacheMetrics.js +++ b/app/js/ContentCacheMetrics.js @@ -19,6 +19,9 @@ function getSystemLoad() { const ONE_MB = 1024 * 1024 function emitPdfStats(stats, timings) { + if (stats['pdf-caching-timed-out']) { + Metrics.inc('pdf-caching-timed-out') + } if (timings['compute-pdf-caching']) { emitPdfCachingStats(stats, timings) } else { diff --git a/app/js/Errors.js b/app/js/Errors.js index 9b014f8b..0b168034 100644 --- a/app/js/Errors.js +++ b/app/js/Errors.js @@ -4,6 +4,8 @@ */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. +const OError = require('@overleaf/o-error') + let Errors var NotFoundError = function (message) { const error = new Error(message) @@ -29,7 +31,10 @@ var AlreadyCompilingError = function (message) { } AlreadyCompilingError.prototype.__proto__ = Error.prototype +class TimedOutError extends OError {} + module.exports = Errors = { + TimedOutError, NotFoundError, FilesOutOfSyncError, AlreadyCompilingError diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index 4d2e6413..539b1bc9 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -26,6 +26,7 @@ const Metrics = require('./Metrics') const OutputFileOptimiser = require('./OutputFileOptimiser') const ContentCacheManager = require('./ContentCacheManager') +const { TimedOutError } = require('./Errors') module.exports = OutputCacheManager = { CONTENT_SUBDIR: 'content', @@ -281,7 +282,16 @@ module.exports = OutputCacheManager = { contentDir, outputFilePath, pdfSize, + timings.compile, function (err, result) { + if (err && err instanceof TimedOutError) { + logger.warn( + { err, outputDir, stats, timings }, + 'pdf caching timed out' + ) + stats['pdf-caching-timed-out'] = 1 + return callback(null, outputFiles) + } if (err) return callback(err, outputFiles) const [contentRanges, newContentRanges, reclaimedSpace] = result diff --git a/app/lib/pdfjs/FSPdfManager.js b/app/lib/pdfjs/FSPdfManager.js index a0450c1e..8fb1606b 100644 --- a/app/lib/pdfjs/FSPdfManager.js +++ b/app/lib/pdfjs/FSPdfManager.js @@ -4,10 +4,10 @@ const { MissingDataException } = require('pdfjs-dist/lib/core/core_utils') const { FSStream } = require('./FSStream') class FSPdfManager extends LocalPdfManager { - constructor(docId, options) { + constructor(docId, { fh, size, checkDeadline }) { const nonEmptyDummyBuffer = Buffer.alloc(1, 0) super(docId, nonEmptyDummyBuffer) - this.stream = new FSStream(options.fh, 0, options.size) + this.stream = new FSStream(fh, 0, size, null, null, checkDeadline) this.pdfDocument = new PDFDocument(this, this.stream) } diff --git a/app/lib/pdfjs/FSStream.js b/app/lib/pdfjs/FSStream.js index 9179e83f..748d7436 100644 --- a/app/lib/pdfjs/FSStream.js +++ b/app/lib/pdfjs/FSStream.js @@ -4,11 +4,12 @@ const { MissingDataException } = require('pdfjs-dist/lib/core/core_utils') const BUF_SIZE = 1024 // read from the file in 1024 byte pages class FSStream extends Stream { - constructor(fh, start, length, dict, cachedBytes) { + constructor(fh, start, length, dict, cachedBytes, checkDeadline) { const nonEmptyDummyBuffer = Buffer.alloc(1, 0) super(nonEmptyDummyBuffer, start, length, dict) delete this.bytes this.fh = fh + this.checkDeadline = checkDeadline this.cachedBytes = cachedBytes || [] } @@ -23,6 +24,7 @@ class FSStream extends Stream { // Manage cached reads from the file requestRange(begin, end) { + this.checkDeadline(`request range ${begin} - ${end}`) // expand small ranges to read a larger amount if (end - begin < BUF_SIZE) { end = begin + BUF_SIZE @@ -123,6 +125,7 @@ class FSStream extends Stream { } makeSubStream(start, length, dict = null) { + this.checkDeadline(`make sub stream start=${start}/length=${length}`) // BG: had to add this check for null length, it is being called with only // the start value at one point in the xref decoding. The intent is clear // enough @@ -131,7 +134,14 @@ class FSStream extends Stream { if (!length) { length = this.end - start } - return new FSStream(this.fh, start, length, dict, this.cachedBytes) + return new FSStream( + this.fh, + start, + length, + dict, + this.cachedBytes, + this.checkDeadline + ) } } diff --git a/app/lib/pdfjs/parseXrefTable.js b/app/lib/pdfjs/parseXrefTable.js index 4db1e0cf..de7a386f 100644 --- a/app/lib/pdfjs/parseXrefTable.js +++ b/app/lib/pdfjs/parseXrefTable.js @@ -1,18 +1,21 @@ const fs = require('fs') const { FSPdfManager } = require('./FSPdfManager') -async function parseXrefTable(path, size) { +async function parseXrefTable(path, size, checkDeadline) { if (size === 0) { return [] } const file = await fs.promises.open(path) try { - const manager = new FSPdfManager(0, { fh: file, size }) + const manager = new FSPdfManager(0, { fh: file, size, checkDeadline }) await manager.ensureDoc('checkHeader') + checkDeadline('pdfjs: after checkHeader') await manager.ensureDoc('parseStartXRef') + checkDeadline('pdfjs: after parseStartXRef') await manager.ensureDoc('parse') + checkDeadline('pdfjs: after parse') return manager.pdfDocument.catalog.xref.entries } finally { file.close() diff --git a/config/settings.defaults.js b/config/settings.defaults.js index b65a0193..ae6086f1 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -68,7 +68,9 @@ module.exports = { enablePdfCaching: process.env.ENABLE_PDF_CACHING === 'true', enablePdfCachingDark: process.env.ENABLE_PDF_CACHING_DARK === 'true', pdfCachingMinChunkSize: - parseInt(process.env.PDF_CACHING_MIN_CHUNK_SIZE, 10) || 1024 + parseInt(process.env.PDF_CACHING_MIN_CHUNK_SIZE, 10) || 1024, + pdfCachingMaxProcessingTime: + parseInt(process.env.PDF_CACHING_MAX_PROCESSING_TIME, 10) || 10 * 1000 } if (process.env.ALLOWED_COMPILE_GROUPS) { diff --git a/test/unit/lib/pdfjsTests.js b/test/unit/lib/pdfjsTests.js index 64f062c0..72b36a33 100644 --- a/test/unit/lib/pdfjsTests.js +++ b/test/unit/lib/pdfjsTests.js @@ -33,7 +33,7 @@ async function loadContext(example) { } async function backFillSnapshot(example, size) { - const table = await parseXrefTable(pdfPath(example), size) + const table = await parseXrefTable(pdfPath(example), size, () => {}) await fs.promises.mkdir(Path.dirname(snapshotPath(example)), { recursive: true }) @@ -53,6 +53,24 @@ describe('pdfjs', function () { }) }) + describe('when the operation times out', function () { + it('should bail out', async function () { + const path = pdfPath(EXAMPLES[0]) + const { size } = await loadContext(EXAMPLES[0]) + const err = new Error() + let table + try { + table = await parseXrefTable(path, size, () => { + throw err + }) + } catch (e) { + expect(e).to.equal(err) + return + } + expect(table).to.not.exist + }) + }) + for (const example of EXAMPLES) { describe(example, function () { let size, snapshot @@ -70,7 +88,7 @@ describe('pdfjs', function () { }) it('should produce the expected xRef table', async function () { - const table = await parseXrefTable(pdfPath(example), size) + const table = await parseXrefTable(pdfPath(example), size, () => {}) expect(table).to.deep.equal(snapshot) }) }) From 756d4a14a18254e217330c606e2a74416af5ff3d Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 23 Jun 2021 14:42:39 +0100 Subject: [PATCH 694/709] [misc] ContentCacheManager: apply review feedback - count stages - lower bound is 1s Co-Authored-By: Brian Gough <brian.gough@overleaf.com> --- app/js/ContentCacheManager.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index cbd6f3cd..34e8a8cd 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -235,22 +235,26 @@ async function writePdfStream(dir, hash, buffer) { function getDeadlineChecker(compileTime) { const maxOverhead = Math.min( - // Adding 10s to a 40s compile time is OK. - compileTime / 4, + // Adding 10s to a 40s compile time is OK. + // Adding 1s to a 3s compile time is OK. + Math.max(compileTime / 4, 1000), // Adding 30s to a 120s compile time is not OK, limit to 10s. Settings.pdfCachingMaxProcessingTime ) const deadline = Date.now() + maxOverhead let lastStage = { stage: 'start', now: Date.now() } + let completedStages = 0 return function (stage) { const now = Date.now() if (now > deadline) { throw new TimedOutError(stage, { + completedStages, lastStage: lastStage.stage, diffToLastStage: now - lastStage.now }) } + completedStages++ lastStage = { stage, now } } } From f7b49ddd156c87ff2dec3d7d1fcdbf6daae42596 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Wed, 30 Jun 2021 11:45:34 +0100 Subject: [PATCH 695/709] [misc] add timings for the sync stage and output stage --- app/js/CompileManager.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 1d681e8d..765c5e4d 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -114,7 +114,7 @@ module.exports = CompileManager = { }, 'written files to disk' ) - timer.done() + const syncStage = timer.done() const injectDraftModeIfRequired = function (callback) { if (request.draft) { @@ -295,6 +295,8 @@ module.exports = CompileManager = { // Emit compile time. timings.compile = ts + timer = new Metrics.Timer('process-output-files') + return OutputFileFinder.findOutputFiles( resourceList, compileDir, @@ -319,6 +321,10 @@ module.exports = CompileManager = { ) } + const outputStage = timer.done() + timings.sync = syncStage + timings.output = outputStage + // Emit e2e compile time. timings.compileE2E = timerE2E.done() From 8e816f71d7bce0ef1ab4e1d426ae3a56b54e13bc Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <das7pad@outlook.com> Date: Thu, 1 Jul 2021 11:57:03 +0100 Subject: [PATCH 696/709] Revert "very basic mvp for shedding load" --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index b2c261bc..8dd8a8cb 100644 --- a/app.js +++ b/app.js @@ -331,7 +331,7 @@ const loadTcpServer = net.createServer(function (socket) { const freeLoad = availableWorkingCpus - currentLoad let freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100) if (freeLoadPercentage <= 0) { - freeLoadPercentage = 0 // when its 0 the server is set to drain and will move projects to different servers + freeLoadPercentage = 1 // when its 0 the server is set to drain and will move projects to different servers } socket.write(`up, ${freeLoadPercentage}%\n`, 'ASCII') return socket.end() From 58b198e98b574718a81e83b995439638ef476be2 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Fri, 2 Jul 2021 09:17:29 +0100 Subject: [PATCH 697/709] [UrlFetcher] do not override domain for clsi-perf requests --- app/js/UrlFetcher.js | 9 ++++++--- config/settings.defaults.js | 5 +++++ test/unit/js/UrlFetcherTests.js | 21 ++++++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/js/UrlFetcher.js b/app/js/UrlFetcher.js index b3ed8c46..adfb55da 100644 --- a/app/js/UrlFetcher.js +++ b/app/js/UrlFetcher.js @@ -42,9 +42,12 @@ module.exports = UrlFetcher = { return (_callback = function () {}) } - if (settings.filestoreDomainOveride != null) { - const p = URL.parse(url).path - url = `${settings.filestoreDomainOveride}${p}` + const u = URL.parse(url) + if ( + settings.filestoreDomainOveride && + u.host !== settings.apis.clsiPerf.host + ) { + url = `${settings.filestoreDomainOveride}${u.path}` } var timeoutHandler = setTimeout( function () { diff --git a/config/settings.defaults.js b/config/settings.defaults.js index ae6086f1..b53f871b 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -51,6 +51,11 @@ module.exports = { apis: { clsi: { url: `http://${process.env.CLSI_HOST || 'localhost'}:3013` + }, + clsiPerf: { + host: `${process.env.CLSI_PERF_HOST || 'localhost'}:${ + process.env.CLSI_PERF_PORT || '3043' + }` } }, diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index 238e5d8a..6a5bc1f3 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -24,7 +24,13 @@ describe('UrlFetcher', function () { defaults: (this.defaults = sinon.stub().returns((this.request = {}))) }, fs: (this.fs = {}), - 'settings-sharelatex': (this.settings = {}) + 'settings-sharelatex': (this.settings = { + apis: { + clsiPerf: { + host: 'localhost:3043' + } + } + }) } })) }) @@ -94,6 +100,19 @@ describe('UrlFetcher', function () { return this.fileStream.emit('finish') }) + it('should not use override clsiPerf domain when filestoreDomainOveride is set', function (done) { + this.settings.filestoreDomainOveride = '192.11.11.11' + const url = 'http://localhost:3043/file/here?query=string' + this.UrlFetcher.pipeUrlToFile(url, this.path, () => { + this.request.get.args[0][0].url.should.equal(url) + done() + }) + this.res = { statusCode: 200 } + this.urlStream.emit('response', this.res) + this.urlStream.emit('end') + this.fileStream.emit('finish') + }) + return it('should use override domain when filestoreDomainOveride is set', function (done) { this.settings.filestoreDomainOveride = '192.11.11.11' this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => { From 524ee59669c448c112dfd9886fa3b3f945161d98 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 12 Jul 2021 17:35:51 +0100 Subject: [PATCH 698/709] [misc] install bunyan as production dependency ``` Error: Cannot find module 'bunyan' Require stack: - .../node_modules/@google-cloud/logging-bunyan/build/src/middleware/express.js - .../node_modules/@google-cloud/logging-bunyan/build/src/index.js - .../node_modules/logger-sharelatex/logging-manager.js - .../node_modules/logger-sharelatex/index.js - .../app.js ``` --- package-lock.json | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f542d9f5..418dbab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1809,13 +1809,12 @@ "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==" }, "bunyan": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", - "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", - "dev": true, + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", "requires": { "dtrace-provider": "~0.8", - "moment": "^2.10.6", + "moment": "^2.19.3", "mv": "~2", "safe-json-stringify": "~1" } diff --git a/package.json b/package.json index 467242c0..b29aad82 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@overleaf/o-error": "^3.3.1", "async": "3.2.0", "body-parser": "^1.19.0", + "bunyan": "^1.8.15", "diskusage": "^1.1.3", "dockerode": "^3.1.0", "express": "^4.17.1", @@ -44,7 +45,6 @@ }, "devDependencies": { "babel-eslint": "^10.1.0", - "bunyan": "^1.8.12", "chai": "~4.2.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.0", From a95e94b02223bdb7780dc7274dcc3834a45a5b33 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 12 Jul 2021 17:47:21 +0100 Subject: [PATCH 699/709] [misc] switch from settings-sharelatex to @overleaf/settings --- app.js | 2 +- app/js/CommandRunner.js | 2 +- app/js/CompileController.js | 2 +- app/js/CompileManager.js | 2 +- app/js/ContentCacheManager.js | 2 +- app/js/ContentController.js | 2 +- app/js/DbQueue.js | 2 +- app/js/DockerRunner.js | 2 +- app/js/LatexRunner.js | 2 +- app/js/LockManager.js | 2 +- app/js/OutputCacheManager.js | 2 +- app/js/ProjectPersistenceManager.js | 2 +- app/js/RequestParser.js | 2 +- app/js/ResourceWriter.js | 2 +- app/js/StaticServerForbidSymlinks.js | 2 +- app/js/UrlCache.js | 2 +- app/js/UrlFetcher.js | 2 +- app/js/db.js | 2 +- package-lock.json | 18 +++++------------- package.json | 2 +- test/acceptance/js/Stats.js | 2 +- test/acceptance/js/helpers/Client.js | 2 +- test/acceptance/js/helpers/ClsiApp.js | 2 +- test/load/js/loadTest.js | 2 +- test/smoke/js/SmokeTests.js | 2 +- test/unit/js/CompileControllerTests.js | 2 +- test/unit/js/CompileManagerTests.js | 2 +- test/unit/js/ContentCacheManagerTests.js | 2 +- test/unit/js/DockerLockManagerTests.js | 2 +- test/unit/js/DockerRunnerTests.js | 2 +- test/unit/js/LatexRunnerTests.js | 2 +- test/unit/js/LockManagerTests.js | 2 +- test/unit/js/ProjectPersistenceManagerTests.js | 2 +- test/unit/js/RequestParserTests.js | 2 +- .../unit/js/StaticServerForbidSymlinksTests.js | 2 +- test/unit/js/UrlCacheTests.js | 2 +- test/unit/js/UrlFetcherTests.js | 2 +- 37 files changed, 41 insertions(+), 49 deletions(-) diff --git a/app.js b/app.js index 8dd8a8cb..214d20fb 100644 --- a/app.js +++ b/app.js @@ -11,7 +11,7 @@ Metrics.initialize('clsi') const CompileController = require('./app/js/CompileController') const ContentController = require('./app/js/ContentController') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const logger = require('logger-sharelatex') logger.initialize('clsi') if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { diff --git a/app/js/CommandRunner.js b/app/js/CommandRunner.js index 8e07dacf..782707b3 100644 --- a/app/js/CommandRunner.js +++ b/app/js/CommandRunner.js @@ -6,7 +6,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let commandRunnerPath -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const logger = require('logger-sharelatex') if ((Settings.clsi != null ? Settings.clsi.dockerRunner : undefined) === true) { diff --git a/app/js/CompileController.js b/app/js/CompileController.js index 63a4106a..acb6626a 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -15,7 +15,7 @@ let CompileController const RequestParser = require('./RequestParser') const CompileManager = require('./CompileManager') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const Metrics = require('./Metrics') const ProjectPersistenceManager = require('./ProjectPersistenceManager') const logger = require('logger-sharelatex') diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index 765c5e4d..f56bfafa 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -20,7 +20,7 @@ const ResourceWriter = require('./ResourceWriter') const LatexRunner = require('./LatexRunner') const OutputFileFinder = require('./OutputFileFinder') const OutputCacheManager = require('./OutputCacheManager') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const Path = require('path') const logger = require('logger-sharelatex') const Metrics = require('./Metrics') diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 34e8a8cd..7dfc40f0 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -6,7 +6,7 @@ const { callbackify } = require('util') const fs = require('fs') const crypto = require('crypto') const Path = require('path') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const OError = require('@overleaf/o-error') const pLimit = require('p-limit') const { parseXrefTable } = require('../lib/pdfjs/parseXrefTable') diff --git a/app/js/ContentController.js b/app/js/ContentController.js index 76478def..b154bea1 100644 --- a/app/js/ContentController.js +++ b/app/js/ContentController.js @@ -1,6 +1,6 @@ const Path = require('path') const send = require('send') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const OutputCacheManager = require('./OutputCacheManager') const ONE_DAY_S = 24 * 60 * 60 diff --git a/app/js/DbQueue.js b/app/js/DbQueue.js index 7589370c..ca2155d2 100644 --- a/app/js/DbQueue.js +++ b/app/js/DbQueue.js @@ -6,7 +6,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const async = require('async') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const logger = require('logger-sharelatex') const queue = async.queue( (task, cb) => task(cb), diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 0ff81095..2d0810d1 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -1,4 +1,4 @@ -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const logger = require('logger-sharelatex') const Docker = require('dockerode') const dockerode = new Docker() diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index 48000219..7cd67a03 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -15,7 +15,7 @@ */ let LatexRunner const Path = require('path') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const logger = require('logger-sharelatex') const Metrics = require('./Metrics') const CommandRunner = require('./CommandRunner') diff --git a/app/js/LockManager.js b/app/js/LockManager.js index 1246cc98..a3bdf1bc 100644 --- a/app/js/LockManager.js +++ b/app/js/LockManager.js @@ -12,7 +12,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let LockManager -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const logger = require('logger-sharelatex') const Lockfile = require('lockfile') // from https://github.com/npm/lockfile const Errors = require('./Errors') diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index 539b1bc9..682b85aa 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -20,7 +20,7 @@ const fse = require('fs-extra') const Path = require('path') const logger = require('logger-sharelatex') const _ = require('lodash') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const crypto = require('crypto') const Metrics = require('./Metrics') diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index 8145cccd..814f7e99 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -20,7 +20,7 @@ const dbQueue = require('./DbQueue') const async = require('async') const logger = require('logger-sharelatex') const oneDay = 24 * 60 * 60 * 1000 -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const diskusage = require('diskusage') const { callbackify } = require('util') diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index 66c917ae..702cf559 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -18,7 +18,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let RequestParser -const settings = require('settings-sharelatex') +const settings = require('@overleaf/settings') module.exports = RequestParser = { VALID_COMPILERS: ['pdflatex', 'latex', 'xelatex', 'lualatex'], diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index d8cc2a4c..25f1b979 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -23,7 +23,7 @@ const OutputFileFinder = require('./OutputFileFinder') const ResourceStateManager = require('./ResourceStateManager') const Metrics = require('./Metrics') const logger = require('logger-sharelatex') -const settings = require('settings-sharelatex') +const settings = require('@overleaf/settings') const parallelFileDownloads = settings.parallelFileDownloads || 1 diff --git a/app/js/StaticServerForbidSymlinks.js b/app/js/StaticServerForbidSymlinks.js index edde7774..bcfc5015 100644 --- a/app/js/StaticServerForbidSymlinks.js +++ b/app/js/StaticServerForbidSymlinks.js @@ -17,7 +17,7 @@ let ForbidSymlinks const Path = require('path') const fs = require('fs') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const logger = require('logger-sharelatex') const url = require('url') diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index a8b2b19b..b6378a55 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -16,7 +16,7 @@ let UrlCache const db = require('./db') const dbQueue = require('./DbQueue') const UrlFetcher = require('./UrlFetcher') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const crypto = require('crypto') const fs = require('fs') const logger = require('logger-sharelatex') diff --git a/app/js/UrlFetcher.js b/app/js/UrlFetcher.js index adfb55da..28155b94 100644 --- a/app/js/UrlFetcher.js +++ b/app/js/UrlFetcher.js @@ -16,7 +16,7 @@ let UrlFetcher const request = require('request').defaults({ jar: false }) const fs = require('fs') const logger = require('logger-sharelatex') -const settings = require('settings-sharelatex') +const settings = require('@overleaf/settings') const URL = require('url') const async = require('async') diff --git a/app/js/db.js b/app/js/db.js index 6b7e50ec..135f3a52 100644 --- a/app/js/db.js +++ b/app/js/db.js @@ -9,7 +9,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const Sequelize = require('sequelize') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const _ = require('lodash') const logger = require('logger-sharelatex') diff --git a/package-lock.json b/package-lock.json index 418dbab1..77bb2cb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1022,6 +1022,11 @@ } } }, + "@overleaf/settings": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@overleaf/settings/-/settings-2.1.1.tgz", + "integrity": "sha512-vcJwqCGFKmQxTP/syUqCeMaSRjHmBcQgKOACR9He2uJcErg2GZPa1go+nGvszMbkElM4HfRKm/MfxvqHhoN4TQ==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2065,11 +2070,6 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, - "coffee-script": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", - "integrity": "sha1-gIs5bhEPU9AhoZpO8fZb4OjjX6M=" - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -7047,14 +7047,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, - "settings-sharelatex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/settings-sharelatex/-/settings-sharelatex-1.1.0.tgz", - "integrity": "sha512-f7D+0lnlohoteSn6IKTH72NE+JnAdMWTKwQglAuimZWTID2FRRItZSGeYMTRpvEnaQApkoVwRp//WRMsiddnqw==", - "requires": { - "coffee-script": "1.6.0" - } - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", diff --git a/package.json b/package.json index b29aad82..8a065d5a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@overleaf/metrics": "^3.5.1", "@overleaf/o-error": "^3.3.1", + "@overleaf/settings": "^2.1.1", "async": "3.2.0", "body-parser": "^1.19.0", "bunyan": "^1.8.15", @@ -38,7 +39,6 @@ "request": "^2.88.2", "send": "^0.17.1", "sequelize": "^5.21.5", - "settings-sharelatex": "^1.1.0", "sqlite3": "^4.1.1", "v8-profiler-node8": "^6.1.1", "wrench": "~1.5.9" diff --git a/test/acceptance/js/Stats.js b/test/acceptance/js/Stats.js index 87b20b1c..d96a8fcb 100644 --- a/test/acceptance/js/Stats.js +++ b/test/acceptance/js/Stats.js @@ -1,5 +1,5 @@ const request = require('request') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') after(function (done) { request( { diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index c5814ff3..1da6601e 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -15,7 +15,7 @@ let Client const request = require('request') const fs = require('fs') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const host = 'localhost' diff --git a/test/acceptance/js/helpers/ClsiApp.js b/test/acceptance/js/helpers/ClsiApp.js index 160b07e5..343c3c7d 100644 --- a/test/acceptance/js/helpers/ClsiApp.js +++ b/test/acceptance/js/helpers/ClsiApp.js @@ -15,7 +15,7 @@ const app = require('../../../../app') require('logger-sharelatex').logger.level('info') const logger = require('logger-sharelatex') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') module.exports = { running: false, diff --git a/test/load/js/loadTest.js b/test/load/js/loadTest.js index 65090b8b..7e2ae5fa 100644 --- a/test/load/js/loadTest.js +++ b/test/load/js/loadTest.js @@ -10,7 +10,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const request = require('request') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const async = require('async') const fs = require('fs') const _ = require('lodash') diff --git a/test/smoke/js/SmokeTests.js b/test/smoke/js/SmokeTests.js index 8cfb190b..8dbdc0a3 100644 --- a/test/smoke/js/SmokeTests.js +++ b/test/smoke/js/SmokeTests.js @@ -1,5 +1,5 @@ const request = require('request') -const Settings = require('settings-sharelatex') +const Settings = require('@overleaf/settings') const buildUrl = (path) => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}` diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index 3b29c867..e8739379 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -66,7 +66,7 @@ describe('CompileController', function () { requires: { './CompileManager': (this.CompileManager = {}), './RequestParser': (this.RequestParser = {}), - 'settings-sharelatex': (this.Settings = { + '@overleaf/settings': (this.Settings = { apis: { clsi: { url: 'http://clsi.example.com' diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index 97e318a4..7f4046c2 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -31,7 +31,7 @@ describe('CompileManager', function () { './ResourceWriter': (this.ResourceWriter = {}), './OutputFileFinder': (this.OutputFileFinder = {}), './OutputCacheManager': (this.OutputCacheManager = {}), - 'settings-sharelatex': (this.Settings = { + '@overleaf/settings': (this.Settings = { path: { compilesDir: '/compiles/dir', outputDir: '/output/dir' diff --git a/test/unit/js/ContentCacheManagerTests.js b/test/unit/js/ContentCacheManagerTests.js index 76c517d8..6e8490ba 100644 --- a/test/unit/js/ContentCacheManagerTests.js +++ b/test/unit/js/ContentCacheManagerTests.js @@ -8,7 +8,7 @@ describe('ContentCacheManager', function () { let contentDir, pdfPath let ContentCacheManager, files, Settings before(function () { - Settings = require('settings-sharelatex') + Settings = require('@overleaf/settings') ContentCacheManager = require(MODULE_PATH) }) let contentRanges, newContentRanges, reclaimed diff --git a/test/unit/js/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js index 6adfa123..f06f1afa 100644 --- a/test/unit/js/DockerLockManagerTests.js +++ b/test/unit/js/DockerLockManagerTests.js @@ -20,7 +20,7 @@ describe('LockManager', function () { beforeEach(function () { return (this.LockManager = SandboxedModule.require(modulePath, { requires: { - 'settings-sharelatex': (this.Settings = { clsi: { docker: {} } }) + '@overleaf/settings': (this.Settings = { clsi: { docker: {} } }) } })) }) diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index 67290e44..b47ed609 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -28,7 +28,7 @@ describe('DockerRunner', function () { this.container = container = {} this.DockerRunner = SandboxedModule.require(modulePath, { requires: { - 'settings-sharelatex': (this.Settings = { + '@overleaf/settings': (this.Settings = { clsi: { docker: {} }, path: {} }), diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index f763f39c..13858ff4 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -23,7 +23,7 @@ describe('LatexRunner', function () { let Timer this.LatexRunner = SandboxedModule.require(modulePath, { requires: { - 'settings-sharelatex': (this.Settings = { + '@overleaf/settings': (this.Settings = { docker: { socketPath: '/var/run/docker.sock' } diff --git a/test/unit/js/LockManagerTests.js b/test/unit/js/LockManagerTests.js index 58580583..4f9cc31f 100644 --- a/test/unit/js/LockManagerTests.js +++ b/test/unit/js/LockManagerTests.js @@ -22,7 +22,7 @@ describe('DockerLockManager', function () { beforeEach(function () { this.LockManager = SandboxedModule.require(modulePath, { requires: { - 'settings-sharelatex': {}, + '@overleaf/settings': {}, fs: { lstat: sinon.stub().callsArgWith(1), readdir: sinon.stub().callsArgWith(1) diff --git a/test/unit/js/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js index e60de54f..be8050aa 100644 --- a/test/unit/js/ProjectPersistenceManagerTests.js +++ b/test/unit/js/ProjectPersistenceManagerTests.js @@ -27,7 +27,7 @@ describe('ProjectPersistenceManager', function () { './UrlCache': (this.UrlCache = {}), './CompileManager': (this.CompileManager = {}), diskusage: (this.diskusage = { check: sinon.stub() }), - 'settings-sharelatex': (this.settings = { + '@overleaf/settings': (this.settings = { project_cache_length_ms: 1000, path: { compilesDir: '/compiles', diff --git a/test/unit/js/RequestParserTests.js b/test/unit/js/RequestParserTests.js index ff2ef7a9..840e55dd 100644 --- a/test/unit/js/RequestParserTests.js +++ b/test/unit/js/RequestParserTests.js @@ -40,7 +40,7 @@ describe('RequestParser', function () { } return (this.RequestParser = SandboxedModule.require(modulePath, { requires: { - 'settings-sharelatex': (this.settings = {}) + '@overleaf/settings': (this.settings = {}) } })) }) diff --git a/test/unit/js/StaticServerForbidSymlinksTests.js b/test/unit/js/StaticServerForbidSymlinksTests.js index 65a66f3d..86e279e6 100644 --- a/test/unit/js/StaticServerForbidSymlinksTests.js +++ b/test/unit/js/StaticServerForbidSymlinksTests.js @@ -30,7 +30,7 @@ describe('StaticServerForbidSymlinks', function () { this.fs = {} this.ForbidSymlinks = SandboxedModule.require(modulePath, { requires: { - 'settings-sharelatex': this.settings, + '@overleaf/settings': this.settings, fs: this.fs } }) diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index 40652c58..32e175f7 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -24,7 +24,7 @@ describe('UrlCache', function () { requires: { './db': {}, './UrlFetcher': (this.UrlFetcher = {}), - 'settings-sharelatex': (this.Settings = { + '@overleaf/settings': (this.Settings = { path: { clsiCacheDir: '/cache/dir' } }), fs: (this.fs = { copyFile: sinon.stub().yields() }) diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index 6a5bc1f3..ac94540f 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -24,7 +24,7 @@ describe('UrlFetcher', function () { defaults: (this.defaults = sinon.stub().returns((this.request = {}))) }, fs: (this.fs = {}), - 'settings-sharelatex': (this.settings = { + '@overleaf/settings': (this.settings = { apis: { clsiPerf: { host: 'localhost:3043' From 92910936e13f9e3a9d7f032403de64ee83a43078 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 12 Jul 2021 17:51:07 +0100 Subject: [PATCH 700/709] [misc] run npm dedupe --- package-lock.json | 431 ++++------------------------------------------ 1 file changed, 37 insertions(+), 394 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77bb2cb4..122b9e6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,25 +193,6 @@ "jws": "^4.0.0", "lru-cache": "^5.0.0" } - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } } } }, @@ -269,11 +250,6 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" }, - "coffeescript": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", - "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" - }, "duplexify": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", @@ -361,14 +337,6 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -453,25 +421,6 @@ "lru-cache": "^5.0.0" } }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, "type-fest": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", @@ -1094,14 +1043,6 @@ "dev": true, "requires": { "type-detect": "4.0.8" - }, - "dependencies": { - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - } } }, "@sinonjs/fake-timers": { @@ -1132,14 +1073,6 @@ "@sinonjs/commons": "^1.6.0", "lodash.get": "^4.4.2", "type-detect": "^4.0.8" - }, - "dependencies": { - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - } } }, "@sinonjs/text-encoding": { @@ -1295,6 +1228,11 @@ "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==" + }, "ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", @@ -1438,14 +1376,6 @@ "requires": { "ast-types-flow": "0.0.7", "commander": "^2.11.0" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - } } }, "array-flatten": { @@ -1914,12 +1844,6 @@ "supports-color": "^5.3.0" }, "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2070,6 +1994,11 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "coffeescript": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", + "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2093,6 +2022,12 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "common-tags": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", @@ -2366,6 +2301,12 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "diskusage": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.1.3.tgz", @@ -2456,14 +2397,6 @@ "optional": true, "requires": { "nan": "^2.14.0" - }, - "dependencies": { - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "optional": true - } } }, "duplexer3": { @@ -2643,18 +2576,6 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -2670,12 +2591,6 @@ "ms": "^2.1.1" } }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -2685,21 +2600,6 @@ "type-fest": "^0.8.1" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3573,25 +3473,6 @@ "bignumber.js": "^9.0.0" } }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3655,25 +3536,6 @@ "lru-cache": "^5.0.0" } }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, "protobufjs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", @@ -3740,6 +3602,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "gtoken": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", @@ -3751,25 +3619,6 @@ "mime": "^2.2.0" }, "dependencies": { - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, "mime": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", @@ -3844,13 +3693,6 @@ "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==", "requires": { "nan": "^2.13.2" - }, - "dependencies": { - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - } } }, "hex2dec": { @@ -3892,14 +3734,6 @@ "debug": "4" }, "dependencies": { - "agent-base": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", - "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", - "requires": { - "debug": "4" - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -3934,14 +3768,6 @@ "debug": "4" }, "dependencies": { - "agent-base": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", - "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", - "requires": { - "debug": "4" - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -4116,12 +3942,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "run-async": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", @@ -4510,12 +4330,6 @@ "strip-bom": "^3.0.0" }, "dependencies": { - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -4613,17 +4427,6 @@ "yn": "^4.0.0" }, "dependencies": { - "bunyan": { - "version": "1.8.14", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz", - "integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, "yn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yn/-/yn-4.0.0.tgz", @@ -4906,12 +4709,6 @@ "ms": "^2.1.1" } }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -4921,12 +4718,6 @@ "locate-path": "^3.0.0" } }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -4937,15 +4728,6 @@ "path-exists": "^3.0.0" } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -5027,15 +4809,6 @@ "path-is-absolute": "^1.0.0" } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, "rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", @@ -5713,14 +5486,6 @@ "tar": "^4.4.2" } }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, "protobufjs": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", @@ -5787,18 +5552,6 @@ "vue-eslint-parser": "^2.0.2" }, "dependencies": { - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -5894,12 +5647,6 @@ "eslint-visitor-keys": "^1.0.0" } }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -5965,15 +5712,6 @@ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6057,18 +5795,6 @@ "yargs": "^13.2.4" }, "dependencies": { - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -6172,12 +5898,6 @@ "eslint-visitor-keys": "^1.0.0" } }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -6288,15 +6008,6 @@ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6742,15 +6453,6 @@ "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } } } }, @@ -7388,18 +7090,6 @@ "string-width": "^3.0.0" }, "dependencies": { - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -7412,24 +7102,12 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -7464,16 +7142,6 @@ "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", "yallist": "^3.0.2" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - } } }, "tar-fs": { @@ -7485,16 +7153,6 @@ "mkdirp": "^0.5.1", "pump": "^3.0.0", "tar-stream": "^2.0.0" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - } } }, "tar-stream": { @@ -7665,6 +7323,15 @@ } } }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -7886,19 +7553,6 @@ "node-pre-gyp": "^0.13.0" }, "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - }, "node-pre-gyp": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz", @@ -8175,17 +7829,6 @@ "dev": true, "requires": { "mkdirp": "^0.5.1" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - } } }, "write-file-atomic": { From c48fcf50990bf79749fdb5e3b3334b786ff80d18 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 13 Jul 2021 10:07:04 +0100 Subject: [PATCH 701/709] [misc] goodbye coffee-script --- package-lock.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 122b9e6a..06e971de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1231,7 +1231,25 @@ "agent-base": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", - "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==" + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } }, "ajv": { "version": "6.12.0", From 0bacd2331d277ae5553c44398dd7efdb07b3b892 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 13 Jul 2021 11:55:19 +0100 Subject: [PATCH 702/709] [misc] upgrade build scripts to version 3.11.0 and cleanup packages ``` npm uninstall prettier-eslint-cli eslint-plugin-standard eslint-plugin-jsx-a11y eslint-plugin-react eslint-config-standard-jsx eslint-config-standard-react babel-eslint npm dedupe ``` --- .eslintrc | 2 +- .github/dependabot.yml | 2 +- .prettierrc | 6 +- buildscript.txt | 2 +- package-lock.json | 3774 ++++++++++++++-------------------------- package.json | 37 +- 6 files changed, 1289 insertions(+), 2534 deletions(-) diff --git a/.eslintrc b/.eslintrc index 321353f9..1c14f50e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,9 +3,9 @@ // https://github.com/sharelatex/sharelatex-dev-environment { "extends": [ + "eslint:recommended", "standard", "prettier", - "prettier/standard" ], "parserOptions": { "ecmaVersion": 2018 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e2c64a33..c8567536 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,4 +20,4 @@ updates: # future if we reorganise teams labels: - "dependencies" - - "Team-Magma" + - "type:maintenance" diff --git a/.prettierrc b/.prettierrc index 24f9ec52..c92c3526 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,10 @@ # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment { + "arrowParens": "avoid", "semi": false, - "singleQuote": true + "singleQuote": true, + "trailingComma": "es5", + "tabWidth": 2, + "useTabs": false } diff --git a/buildscript.txt b/buildscript.txt index 84a43ecb..e04c1353 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -6,4 +6,4 @@ clsi --env-pass-through=TEXLIVE_IMAGE --node-version=12.21.0 --public-repo=True ---script-version=3.8.0 +--script-version=3.11.0 diff --git a/package-lock.json b/package-lock.json index 06e971de..86789caf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,142 +5,80 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { - "@babel/highlight": "^7.8.3" + "@babel/highlight": "^7.10.4" } }, - "@babel/generator": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", - "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", - "dev": true, - "requires": { - "@babel/types": "^7.8.7", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", - "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } + "@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "dev": true }, "@babel/highlight": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", - "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", - "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", - "dev": true - }, - "@babel/runtime": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz", - "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.2" - } - }, - "@babel/runtime-corejs3": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.8.7.tgz", - "integrity": "sha512-sc7A+H4I8kTd7S61dgB9RomXu/C+F4IrRr4Ytze4dnfx7AXEpCrejSNpjx7vq6y/Bak9S6Kbk65a/WgMLtg43Q==", - "dev": true, - "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" }, "dependencies": { - "regenerator-runtime": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", - "integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==", - "dev": true + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } } } }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", - "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "@eslint/eslintrc": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", + "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", "dev": true, "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.8.6", - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -148,20 +86,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" } } }, - "@babel/types": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", - "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, "@google-cloud/common": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz", @@ -240,11 +178,6 @@ "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" }, - "acorn": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz", - "integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==" - }, "bignumber.js": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", @@ -913,6 +846,40 @@ "protobufjs": "^6.8.6" } }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, "@opencensus/core": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.20.tgz", @@ -962,13 +929,6 @@ "integrity": "sha512-1FRBYZO0lbJ0U+FRGZVS8ou6RhEw3e2B86WW/NbtBw554g0h5iC8ESf+juIfPMU/WDf/JDIFbg3eB/LnP2RSow==", "requires": { "core-js": "^3.8.3" - }, - "dependencies": { - "core-js": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.13.0.tgz", - "integrity": "sha512-iWDbiyha1M5vFwPFmQnvRv+tJzGbFAm6XimJUT0NgHYW3xZEs1SkCAcasWSVFxpI2Xb/V1DDJckq3v90+bQnog==" - } } }, "@overleaf/settings": { @@ -1095,23 +1055,11 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, "@types/console-log-level": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz", "integrity": "sha512-x+OscEQwcx5Biair4enH7ov9W+clcqUWaZRaxn5IkT4yNWWjRr2oiYDkY/x1uXSTVZOQ2xlbFQySaQGB+VdXGQ==" }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "@types/fs-extra": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", @@ -1120,12 +1068,6 @@ "@types/node": "*" } }, - "@types/json-schema": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", - "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", - "dev": true - }, "@types/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", @@ -1141,58 +1083,11 @@ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==" }, - "@typescript-eslint/experimental-utils": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", - "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "1.13.0", - "eslint-scope": "^4.0.0" - }, - "dependencies": { - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } - } - }, - "@typescript-eslint/parser": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz", - "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==", - "dev": true, - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "1.13.0", - "@typescript-eslint/typescript-estree": "1.13.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", - "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", - "dev": true, - "requires": { - "lodash.unescape": "4.0.1", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } - } + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true }, "abbrev": { "version": "1.1.1", @@ -1217,15 +1112,14 @@ } }, "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz", + "integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==" }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, "agent-base": { @@ -1312,28 +1206,11 @@ } }, "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -1386,40 +1263,50 @@ "sprintf-js": "~1.0.2" } }, - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", - "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", + "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", "dev": true, "requires": { + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.1.1", "is-string": "^1.0.5" } }, "array.prototype.flat": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", - "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" + } } }, "arrify": { @@ -1446,16 +1333,10 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { @@ -1487,26 +1368,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, - "axobject-query": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz", - "integrity": "sha512-ICt34ZmrVt8UQnvPl6TVyDTkmhXmAyAT4Jh5ugfGUX4MOrZ+U/ZY6/sdylRw3qGNr9Ub5AJsaHeDMzNLehRdOQ==", - "dev": true - }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1608,12 +1469,6 @@ } } }, - "boolify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/boolify/-/boolify-1.0.1.tgz", - "integrity": "sha1-tcCeF8rNET0Rt7s+04TMASmU2Gs=", - "dev": true - }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -1640,26 +1495,14 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -1667,14 +1510,12 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -1706,7 +1547,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -1809,6 +1649,16 @@ } } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1821,17 +1671,6 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "camelcase-keys": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.1.2.tgz", - "integrity": "sha512-QfFrU0CIw2oltVvpndW32kuJ/9YOJwUnmWrjlXt1nnJZHCaS9i6bfOpg9R4Lw8aZjStkJWM+jc0cdXjWBgVJSw==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1851,32 +1690,19 @@ "type-detect": "^4.0.5" } }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "check-error": "^1.0.2" } }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true }, "charenc": { @@ -1923,104 +1749,47 @@ "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", "dev": true }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "restore-cursor": "^3.1.0" + "wrap-ansi": "^7.0.0" } }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "cls-bluebird": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", - "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", - "requires": { - "is-bluebird": "^1.0.2", - "shimmer": "^1.1.0" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "coffeescript": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", - "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "cls-bluebird": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", + "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", + "requires": { + "is-bluebird": "^1.0.2", + "shimmer": "^1.1.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "coffeescript": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", + "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { "color-name": "1.1.3" @@ -2040,18 +1809,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "common-tags": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", - "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", - "dev": true - }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -2133,12 +1890,6 @@ "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.1.tgz", "integrity": "sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ==" }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -2172,16 +1923,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "core-js": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", - "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", - "dev": true - }, - "core-js-pure": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz", - "integrity": "sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==", - "dev": true + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.13.0.tgz", + "integrity": "sha512-iWDbiyha1M5vFwPFmQnvRv+tJzGbFAm6XimJUT0NgHYW3xZEs1SkCAcasWSVFxpI2Xb/V1DDJckq3v90+bQnog==" }, "core-util-is": { "version": "1.0.2", @@ -2189,16 +1933,14 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, "crypt": { @@ -2217,12 +1959,6 @@ "resolved": "https://registry.npmjs.org/d64/-/d64-1.0.0.tgz", "integrity": "sha1-QAKofoUMv8n52XBrYPymE6MzbpA=" }, - "damerau-levenshtein": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", - "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", - "dev": true - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -2240,9 +1976,9 @@ } }, "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, "decompress-response": { @@ -2320,9 +2056,9 @@ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, "diskusage": { @@ -2334,12 +2070,6 @@ "nan": "^2.14.0" } }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, "docker-modem": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.1.tgz", @@ -2467,8 +2197,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "encodeurl": { "version": "1.0.2", @@ -2483,6 +2212,15 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", @@ -2498,22 +2236,24 @@ } }, "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", "dev": true, "requires": { + "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "object-inspect": "^1.10.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" } }, "es-to-primitive": { @@ -2532,6 +2272,12 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -2550,72 +2296,158 @@ "dev": true }, "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz", + "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.2", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.3", + "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "yallist": "^4.0.0" } }, "ms": { @@ -2624,63 +2456,76 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } }, "eslint-config-prettier": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz", - "integrity": "sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==", - "dev": true, - "requires": { - "get-stdin": "^6.0.0" - } - }, - "eslint-config-standard": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz", - "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", + "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", "dev": true }, - "eslint-config-standard-jsx": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-8.1.0.tgz", - "integrity": "sha512-ULVC8qH8qCqbU792ZOO6DaiaZyHNS/5CZt3hKqHkEhVlhPEPN3nfBqqxJCyp59XrjIBZPu1chMYe9T2DXZ7TMw==", + "eslint-config-standard": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", + "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", "dev": true }, - "eslint-config-standard-react": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard-react/-/eslint-config-standard-react-9.2.0.tgz", - "integrity": "sha512-u+KRP2uCtthZ/W4DlLWCC59GZNV/y9k9yicWWammgTs/Omh8ZUUPF3EnYm81MAcbkYQq2Wg0oxutAhi/FQ8mIw==", - "dev": true, - "requires": { - "eslint-config-standard-jsx": "^8.0.0" - } - }, "eslint-import-resolver-node": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", - "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", "dev": true, "requires": { "debug": "^2.6.9", @@ -2688,147 +2533,134 @@ }, "dependencies": { "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "requires": { + "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } } } }, "eslint-module-utils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz", - "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz", + "integrity": "sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A==", "dev": true, "requires": { - "debug": "^2.6.9", + "debug": "^3.2.7", "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } } }, "eslint-plugin-chai-expect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-chai-expect/-/eslint-plugin-chai-expect-2.1.0.tgz", - "integrity": "sha512-rd0/4mjMV6c3i0o4DKkWI4uaFN9DK707kW+/fDphaDI6HVgxXnhML9Xgt5vHnTXmSSnDhupuCFBgsEAEpchXmQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-expect/-/eslint-plugin-chai-expect-2.2.0.tgz", + "integrity": "sha512-ExTJKhgeYMfY8wDj3UiZmgpMKJOUHGNHmWMlxT49JUDB1vTnw0sSNfXJSxnX+LcebyBD/gudXzjzD136WqPJrQ==", "dev": true }, "eslint-plugin-chai-friendly": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.5.0.tgz", - "integrity": "sha512-Pxe6z8C9fP0pn2X2nGFU/b3GBOCM/5FVus1hsMwJsXP3R7RiXFl7g0ksJbsc0GxiLyidTW4mEFk77qsNn7Tk7g==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.6.0.tgz", + "integrity": "sha512-Uvvv1gkbRGp/qfN15B0kQyQWg+oFA8buDSqrwmW3egNSk/FpqH2MjQqKOuKwmEL6w4QIQrIjDp+gg6kGGmD3oQ==", "dev": true }, "eslint-plugin-es": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.0.tgz", - "integrity": "sha512-6/Jb/J/ZvSebydwbBJO1R9E5ky7YeElfK56Veh7e4QGFHCXoIXGH9HhVz+ibJLM3XJ1XjP+T7rKBLUa/Y7eIng==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", "dev": true, "requires": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" - }, - "dependencies": { - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "regexpp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", - "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", - "dev": true - } } }, "eslint-plugin-import": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", - "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "version": "2.23.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.23.4.tgz", + "integrity": "sha512-6/wP8zZRsnQFiR3iaPFgh5ImVRM1WN5NUWfTIRqwOdeiGJlBcSk82o1FEVq8yXmy4lkIzTo7YhHCIxlU/2HyEQ==", "dev": true, "requires": { - "array-includes": "^3.0.3", - "array.prototype.flat": "^1.2.1", - "contains-path": "^0.1.0", + "array-includes": "^3.1.3", + "array.prototype.flat": "^1.2.4", "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.1", - "has": "^1.0.3", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.1", + "find-up": "^2.0.0", + "has": "^1.0.3", + "is-core-module": "^2.4.0", "minimatch": "^3.0.4", - "object.values": "^1.1.0", - "read-pkg-up": "^2.0.0", - "resolve": "^1.12.0" + "object.values": "^1.1.3", + "pkg-up": "^2.0.0", + "read-pkg-up": "^3.0.0", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.9.0" }, "dependencies": { "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "esutils": "^2.0.2" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" } - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true } } }, "eslint-plugin-mocha": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.3.0.tgz", - "integrity": "sha512-Cd2roo8caAyG21oKaaNTj7cqeYRWW1I2B5SfpKRp0Ip1gkfwoR1Ow0IGlPWnNjzywdF4n+kHL8/9vM6zCJUxdg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.2.0.tgz", + "integrity": "sha512-8oOR47Ejt+YJPNQzedbiklDqS1zurEaNrxXpRs+Uk4DMDPVmKNagShFeUaYsfvWP55AhI+P1non5QZAHV6K78A==", "dev": true, "requires": { - "eslint-utils": "^2.0.0", - "ramda": "^0.27.0" - }, - "dependencies": { - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - } + "eslint-utils": "^2.1.0", + "ramda": "^0.27.1" } }, "eslint-plugin-node": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.0.0.tgz", - "integrity": "sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, "requires": { "eslint-plugin-es": "^3.0.0", @@ -2839,19 +2671,10 @@ "semver": "^6.1.0" }, "dependencies": { - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, "semver": { @@ -2877,72 +2700,20 @@ "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", "dev": true }, - "eslint-plugin-react": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", - "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.15.1", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.2", - "xregexp": "^4.3.0" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-plugin-standard": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", - "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", - "dev": true - }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -2955,26 +2726,26 @@ "dev": true }, "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" }, "dependencies": { "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, - "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true } } @@ -2986,21 +2757,26 @@ "dev": true }, "esquery": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", - "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true }, "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "estraverse": "^4.1.0" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "estraverse": { @@ -3083,28 +2859,6 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -3137,22 +2891,13 @@ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" } }, "file-uri-to-path": { @@ -3198,40 +2943,24 @@ "integrity": "sha1-WKRmaX34piBc39vzlVNri9d3pfY=" }, "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true }, "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "flatted": "^3.1.0" } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.1.tgz", + "integrity": "sha512-OMQjaErSFHmHqZe+PSidH5n8j3O0F2DdnVh8JB4j4eUQ2k6KvB0qGfrKIhapvez5JerBbmWkaLYUYWISaESoXg==", "dev": true }, "forever-agent": { @@ -3354,11 +3083,16 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } }, "get-stream": { "version": "4.1.0", @@ -3417,9 +3151,9 @@ } }, "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", "dev": true }, "google-auth-library": { @@ -3667,14 +3401,11 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true }, "has-flag": { "version": "3.0.0", @@ -3856,12 +3587,6 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", @@ -3886,131 +3611,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.5.3", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "run-async": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", - "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "internal-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", - "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", - "dev": true, - "requires": { - "es-abstract": "^1.17.0-next.1", - "has": "^1.0.3", - "side-channel": "^1.0.2" - } - }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4027,6 +3627,12 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "dev": true + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4041,11 +3647,19 @@ "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" }, + "is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { "version": "1.1.5", @@ -4062,6 +3676,15 @@ "ci-info": "^2.0.0" } }, + "is-core-module": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.5.0.tgz", + "integrity": "sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", @@ -4101,6 +3724,12 @@ "is-path-inside": "^3.0.1" } }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, "is-npm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", @@ -4113,6 +3742,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", + "dev": true + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -4124,19 +3759,19 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, - "is-promise": { + "is-plain-obj": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", "dev": true, "requires": { - "has": "^1.0.3" + "call-bind": "^1.0.2" } }, "is-stream": { @@ -4212,12 +3847,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, "json-bigint": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", @@ -4232,6 +3861,12 @@ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -4253,6 +3888,23 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -4273,16 +3925,6 @@ "verror": "1.10.0" } }, - "jsx-ast-utils": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", - "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "object.assign": "^4.1.0" - } - }, "just-extend": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", @@ -4327,31 +3969,31 @@ } }, "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", + "parse-json": "^4.0.0", + "pify": "^3.0.0", "strip-bom": "^3.0.0" }, "dependencies": { "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } } @@ -4389,6 +4031,12 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -4400,22 +4048,16 @@ "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", "dev": true }, "log-driver": { @@ -4424,12 +4066,63 @@ "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==" }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", "dev": true, "requires": { - "chalk": "^2.4.2" + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "logger-sharelatex": { @@ -4452,63 +4145,11 @@ } } }, - "loglevel": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", - "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", - "dev": true - }, - "loglevel-colored-level-prefix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", - "integrity": "sha1-akAhj9x64V/HbD0PPmdsRlOIYD4=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "loglevel": "^1.4.1" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -4540,24 +4181,6 @@ } } }, - "make-plural": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz", - "integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } - }, "map-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", @@ -4571,13 +4194,6 @@ "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - } } }, "media-typer": { @@ -4590,29 +4206,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, - "messageformat": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz", - "integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==", - "dev": true, - "requires": { - "make-plural": "^4.3.0", - "messageformat-formatters": "^2.0.1", - "messageformat-parser": "^4.1.2" - } - }, - "messageformat-formatters": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz", - "integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==", - "dev": true - }, - "messageformat-parser": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.2.tgz", - "integrity": "sha512-7dWuifeyldz7vhEuL96Kwq1fhZXBW+TUfbnHN4UCrCxoXQTYjHnR78eI66Gk9LaLLsAvzPNVJBaa66DRfFNaiA==", - "dev": true - }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4636,12 +4229,6 @@ "mime-db": "1.43.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -4687,101 +4274,234 @@ } }, "mocha": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.0.tgz", - "integrity": "sha512-MymHK8UkU0K15Q/zX7uflZgVoRWiTjy0fXE/QjKts6mowUvGxOdPhZ2qj3b0iZdUrNZlW9LAIMFHB4IW+2b3EQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", "dev": true, "requires": { - "ansi-colors": "3.2.3", + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", "growl": "1.10.5", "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", + "js-yaml": "4.0.0", + "log-symbols": "4.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", + "ms": "2.1.3", + "nanoid": "3.1.20", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", - "yargs-unparser": "1.6.0" + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" }, "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "js-yaml": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "argparse": "^2.0.1" } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^5.0.0" } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { - "p-limit": "^2.0.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } + "p-limit": "^3.0.2" } - } - } - }, - "module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=" - }, - "moment": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz", + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=" + }, + "moment": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz", "integrity": "sha1-dZ6kkayX1UusWtd2mW4qWMwbwiU=" }, "moment-timezone": { @@ -4797,12 +4517,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -4875,6 +4589,12 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, + "nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4902,12 +4622,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "nise": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz", @@ -4938,24 +4652,6 @@ } } }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", @@ -5021,15 +4717,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -5107,9 +4794,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", "dev": true }, "object-keys": { @@ -5119,61 +4806,26 @@ "dev": true }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.entries": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", - "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "object.fromentries": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", - "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, "object.values": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", - "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", + "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", "dev": true, "requires": { + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" + "es-abstract": "^1.18.2" } }, "on-finished": { @@ -5197,27 +4849,18 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" } }, "os-homedir": { @@ -5270,19 +4913,13 @@ "requires": { "p-try": "^1.0.0" } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true } } }, "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, "package-json": { @@ -5320,12 +4957,13 @@ "integrity": "sha512-KbAJuYGUhZkB9gotDiKLnZ7Z3VTacK3fgwmDdB6ZVDtJbMBT6MfLga0WJaYpPDu0mzqT0NgHtHDt5PY4l0nidg==" }, "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { - "error-ex": "^1.2.0" + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" } }, "parse-ms": { @@ -5349,16 +4987,10 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "path-parse": { @@ -5372,18 +5004,18 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "requires": { - "pify": "^2.0.0" + "pify": "^3.0.0" }, "dependencies": { "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } } @@ -5424,6 +5056,15 @@ "find-up": "^2.1.0" } }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, "pprof": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pprof/-/pprof-3.0.0.tgz", @@ -5459,651 +5100,96 @@ "ms": "^2.1.1" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "needle": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", - "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.16.0.tgz", - "integrity": "sha512-4efGA+X/YXAHLi1hN8KaPrILULaUn2nWecFrn1k2I+99HpoyvcOGEbtcOxpDiUwPF2ZANMJDh32qwOUPenuR1g==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.3", - "needle": "^2.5.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "protobufjs": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", - "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": "^13.7.0", - "long": "^4.0.0" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true - }, - "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", - "dev": true - }, - "prettier-eslint": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-9.0.1.tgz", - "integrity": "sha512-KZT65QTosSAqBBqmrC+RpXbsMRe7Os2YSR9cAfFbDlyPAopzA/S5bioiZ3rpziNQNSJaOxmtXSx07EQ+o2Dlug==", - "dev": true, - "requires": { - "@typescript-eslint/parser": "^1.10.2", - "common-tags": "^1.4.0", - "core-js": "^3.1.4", - "dlv": "^1.1.0", - "eslint": "^5.0.0", - "indent-string": "^4.0.0", - "lodash.merge": "^4.6.0", - "loglevel-colored-level-prefix": "^1.0.0", - "prettier": "^1.7.0", - "pretty-format": "^23.0.1", - "require-relative": "^0.8.7", - "typescript": "^3.2.1", - "vue-eslint-parser": "^2.0.2" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "prettier-eslint-cli": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/prettier-eslint-cli/-/prettier-eslint-cli-5.0.0.tgz", - "integrity": "sha512-cei9UbN1aTrz3sQs88CWpvY/10PYTevzd76zoG1tdJ164OhmNTFRKPTOZrutVvscoQWzbnLKkviS3gu5JXwvZg==", - "dev": true, - "requires": { - "arrify": "^2.0.1", - "boolify": "^1.0.0", - "camelcase-keys": "^6.0.0", - "chalk": "^2.4.2", - "common-tags": "^1.8.0", - "core-js": "^3.1.4", - "eslint": "^5.0.0", - "find-up": "^4.1.0", - "get-stdin": "^7.0.0", - "glob": "^7.1.4", - "ignore": "^5.1.2", - "lodash.memoize": "^4.1.2", - "loglevel-colored-level-prefix": "^1.0.0", - "messageformat": "^2.2.1", - "prettier-eslint": "^9.0.0", - "rxjs": "^6.5.2", - "yargs": "^13.2.4" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "dependencies": { - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", - "dev": true - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", - "dev": true - }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "p-limit": "^2.2.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } + "minimist": "^1.2.5" } }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, + "needle": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", + "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, + "node-pre-gyp": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.16.0.tgz", + "integrity": "sha512-4efGA+X/YXAHLi1hN8KaPrILULaUn2nWecFrn1k2I+99HpoyvcOGEbtcOxpDiUwPF2ZANMJDh32qwOUPenuR1g==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" } }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, + "protobufjs": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", + "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", "requires": { - "ansi-regex": "^3.0.0" + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", + "long": "^4.0.0" } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" } } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true + }, "prettier-linter-helpers": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", @@ -6113,24 +5199,6 @@ "fast-diff": "^1.1.2" } }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, "pretty-ms": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", @@ -6158,17 +5226,6 @@ "tdigest": "^0.1.1" } }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, "protobufjs": { "version": "6.8.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", @@ -6270,18 +5327,21 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true - }, "ramda": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", - "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", "dev": true }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -6350,31 +5410,25 @@ } } }, - "react-is": { - "version": "16.13.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz", - "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==", - "dev": true - }, "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", "dev": true, "requires": { - "load-json-file": "^2.0.0", + "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" + "path-type": "^3.0.0" } }, "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", "dev": true, "requires": { "find-up": "^2.0.0", - "read-pkg": "^2.0.0" + "read-pkg": "^3.0.0" } }, "readable-stream": { @@ -6400,26 +5454,10 @@ "picomatch": "^2.0.4" } }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "registry-auth-token": { @@ -6480,6 +5518,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-in-the-middle": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.1.0.tgz", @@ -6511,18 +5555,6 @@ "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", - "dev": true - }, "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", @@ -6546,16 +5578,6 @@ "lowercase-keys": "^1.0.0" } }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, "retry-as-promised": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", @@ -6596,24 +5618,6 @@ "glob": "^7.0.5" } }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -6746,6 +5750,15 @@ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==" }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -6768,18 +5781,18 @@ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" } }, "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "shimmer": { @@ -6787,16 +5800,6 @@ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", "integrity": "sha1-+Wb3VVeJdj502IQRk2haXnhzZmU=" }, - "side-channel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", - "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", - "dev": true, - "requires": { - "es-abstract": "^1.17.0-next.1", - "object-inspect": "^1.7.0" - } - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -6841,20 +5844,19 @@ } }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0" }, "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true } } @@ -6883,9 +5885,9 @@ } }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -6893,15 +5895,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -6909,9 +5911,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", "dev": true }, "split": { @@ -7021,38 +6023,24 @@ "strip-ansi": "^3.0.0" } }, - "string.prototype.matchall": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", - "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", - "has-symbols": "^1.0.1", - "internal-slot": "^1.0.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.2" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, "string_decoder": { @@ -7074,76 +6062,49 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.1.tgz", + "integrity": "sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "fast-deep-equal": "^3.1.1", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" } } } @@ -7260,21 +6221,6 @@ "integrity": "sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A==", "dev": true }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "to-no-case": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", @@ -7350,11 +6296,24 @@ "punycode": "^2.1.1" } }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true + "tsconfig-paths": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz", + "integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==", + "dev": true, + "requires": { + "json5": "^2.2.0", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } }, "tunnel-agent": { "version": "0.6.0", @@ -7370,12 +6329,12 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "prelude-ls": "~1.1.2" + "prelude-ls": "^1.2.1" } }, "type-detect": { @@ -7413,11 +6372,25 @@ "is-typedarray": "^1.0.0" } }, - "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", - "dev": true + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + } + } }, "undefsafe": { "version": "2.0.3", @@ -7477,26 +6450,14 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -7504,20 +6465,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -7557,9 +6515,9 @@ "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "v8-profiler-node8": { @@ -7620,99 +6578,32 @@ "extsprintf": "^1.2.0" } }, - "vue-eslint-parser": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", - "integrity": "sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "eslint-scope": "^3.7.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.2", - "esquery": "^1.0.0", - "lodash": "^4.17.4" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "walkdir": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" }, "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } }, "wide-align": { "version": "1.1.3", @@ -7779,56 +6670,17 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "workerpool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==", + "dev": true + }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true }, "wrappy": { "version": "1.0.2", @@ -7840,15 +6692,6 @@ "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz", "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo=" }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -7867,19 +6710,10 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, - "xregexp": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", - "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", - "dev": true, - "requires": { - "@babel/runtime-corejs3": "^7.8.3" - } - }, "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yallist": { @@ -7888,121 +6722,43 @@ "integrity": "sha1-tLBJ4xS+VF486AIjbWzSLNkcPek=" }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } - } - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true }, "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + } } }, "yn": { diff --git a/package.json b/package.json index 8a065d5a..4f22ebd7 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,10 @@ "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js test/unit/lib", "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", - "lint": "node_modules/.bin/eslint --max-warnings 0 .", - "format": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --list-different", - "format:fix": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --write" + "lint": "eslint --max-warnings 0 --format unix .", + "format": "prettier --list-different $PWD/'**/*.js'", + "format:fix": "prettier --write $PWD/'**/*.js'", + "lint:fix": "eslint --fix ." }, "author": "James Allen <james@sharelatex.com>", "dependencies": { @@ -44,27 +45,21 @@ "wrench": "~1.5.9" }, "devDependencies": { - "babel-eslint": "^10.1.0", - "chai": "~4.2.0", - "eslint": "^6.8.0", - "eslint-config-prettier": "^6.10.0", - "eslint-config-standard": "^14.1.0", - "eslint-config-standard-jsx": "^8.1.0", - "eslint-config-standard-react": "^9.2.0", - "eslint-plugin-chai-expect": "^2.1.0", - "eslint-plugin-chai-friendly": "^0.5.0", - "eslint-plugin-import": "^2.20.1", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-mocha": "^6.3.0", - "eslint-plugin-node": "^11.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "eslint": "^7.21.0", + "eslint-config-prettier": "^8.1.0", + "eslint-config-standard": "^16.0.2", + "eslint-plugin-chai-expect": "^2.2.0", + "eslint-plugin-chai-friendly": "^0.6.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-react": "^7.19.0", - "eslint-plugin-standard": "^4.0.1", - "mocha": "^7.1.0", + "mocha": "^8.3.2", "nodemon": "^2.0.7", - "prettier": "^2.0.0", - "prettier-eslint-cli": "^5.0.0", + "prettier": "^2.2.1", "sandboxed-module": "^2.0.3", "sinon": "~9.0.1", "timekeeper": "2.2.0" From 906bfc9ec89325c033951d2f43f484ddc96469b8 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 13 Jul 2021 12:04:48 +0100 Subject: [PATCH 703/709] [misc] run format_fix and lint:fix --- app.js | 57 +- app/js/CompileController.js | 189 +++---- app/js/CompileManager.js | 500 +++++++++--------- app/js/ContentCacheManager.js | 18 +- app/js/ContentCacheMetrics.js | 8 +- app/js/ContentTypeMapper.js | 2 +- app/js/DockerLockManager.js | 2 +- app/js/DockerRunner.js | 42 +- app/js/DraftModeManager.js | 4 +- app/js/Errors.js | 2 +- app/js/LatexRunner.js | 28 +- app/js/LocalCommandRunner.js | 6 +- app/js/LockManager.js | 6 +- app/js/OutputCacheManager.js | 104 ++-- app/js/OutputFileFinder.js | 14 +- app/js/OutputFileOptimiser.js | 20 +- app/js/ProjectPersistenceManager.js | 43 +- app/js/RequestParser.js | 29 +- app/js/ResourceStateManager.js | 73 +-- app/js/ResourceWriter.js | 251 ++++----- app/js/SafeReader.js | 25 +- app/js/StaticServerForbidSymlinks.js | 6 +- app/js/TikzManager.js | 96 ++-- app/js/UrlCache.js | 116 ++-- app/js/UrlFetcher.js | 2 +- app/js/db.js | 12 +- app/lib/pdfjs/FSPdfManager.js | 2 +- app/lib/pdfjs/FSStream.js | 6 +- app/lib/pdfjs/parseXrefTable.js | 2 +- config/settings.defaults.js | 44 +- test/acceptance/js/AllowedImageNamesTests.js | 14 +- test/acceptance/js/BrokenLatexFileTests.js | 12 +- test/acceptance/js/DeleteOldFilesTest.js | 6 +- test/acceptance/js/ExampleDocumentTests.js | 153 +++--- test/acceptance/js/SimpleLatexFileTests.js | 6 +- test/acceptance/js/Stats.js | 2 +- test/acceptance/js/SynctexTests.js | 18 +- test/acceptance/js/TimeoutTests.js | 10 +- test/acceptance/js/UrlCachingTests.js | 52 +- test/acceptance/js/WordcountTests.js | 10 +- test/acceptance/js/helpers/Client.js | 30 +- test/acceptance/js/helpers/ClsiApp.js | 6 +- test/acceptance/scripts/settings.test.js | 27 +- test/bench/hashbench.js | 18 +- test/load/js/loadTest.js | 19 +- test/setup.js | 6 +- test/smoke/js/SmokeTests.js | 18 +- test/unit/js/CompileControllerTests.js | 84 +-- test/unit/js/CompileManagerTests.js | 66 +-- test/unit/js/ContentCacheManagerTests.js | 34 +- test/unit/js/DockerLockManagerTests.js | 22 +- test/unit/js/DockerRunnerTests.js | 74 +-- test/unit/js/DraftModeManagerTests.js | 4 +- test/unit/js/LatexRunnerTests.js | 26 +- test/unit/js/LockManagerTests.js | 6 +- test/unit/js/OutputFileFinderTests.js | 12 +- test/unit/js/OutputFileOptimiserTests.js | 4 +- .../unit/js/ProjectPersistenceManagerTests.js | 16 +- test/unit/js/RequestParserTests.js | 16 +- test/unit/js/ResourceStateManagerTests.js | 10 +- test/unit/js/ResourceWriterTests.js | 57 +- .../js/StaticServerForbidSymlinksTests.js | 12 +- test/unit/js/TikzManager.js | 4 +- test/unit/js/UrlCacheTests.js | 8 +- test/unit/js/UrlFetcherTests.js | 22 +- test/unit/lib/pdfjsTests.js | 4 +- 66 files changed, 1305 insertions(+), 1292 deletions(-) diff --git a/app.js b/app.js index 214d20fb..8916c0a0 100644 --- a/app.js +++ b/app.js @@ -157,7 +157,7 @@ const staticCompileServer = ForbidSymlinks( res.set('Etag', etag(path, stat)) } return res.set('Content-Type', ContentTypeMapper.map(path)) - } + }, } ) @@ -177,7 +177,7 @@ const staticOutputServer = ForbidSymlinks( res.set('Etag', etag(path, stat)) } return res.set('Content-Type', ContentTypeMapper.map(path)) - } + }, } ) @@ -201,28 +201,29 @@ app.get( ContentController.getPdfRange ) -app.get('/project/:project_id/build/:build_id/output/*', function ( - req, - res, - next -) { - // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) - req.url = - `/${req.params.project_id}/` + - OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) - return staticOutputServer(req, res, next) -}) +app.get( + '/project/:project_id/build/:build_id/output/*', + function (req, res, next) { + // for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId) + req.url = + `/${req.params.project_id}/` + + OutputCacheManager.path(req.params.build_id, `/${req.params[0]}`) + return staticOutputServer(req, res, next) + } +) -app.get('/project/:project_id/user/:user_id/output/*', function ( - req, - res, - next -) { - // for specific user get the path to the top level file - logger.warn({ url: req.url }, 'direct request for file in compile directory') - req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}` - return staticCompileServer(req, res, next) -}) +app.get( + '/project/:project_id/user/:user_id/output/*', + function (req, res, next) { + // for specific user get the path to the top level file + logger.warn( + { url: req.url }, + 'direct request for file in compile directory' + ) + req.url = `/${req.params.project_id}-${req.params.user_id}/${req.params[0]}` + return staticCompileServer(req, res, next) + } +) app.get('/project/:project_id/output/*', function (req, res, next) { logger.warn({ url: req.url }, 'direct request for file in compile directory') @@ -271,7 +272,7 @@ if (Settings.processLifespanLimitMs) { function runSmokeTest() { if (Settings.processTooOld) return logger.log('running smoke tests') - smokeTest.triggerRun((err) => { + smokeTest.triggerRun(err => { if (err) logger.error({ err }, 'smoke tests failed') setTimeout(runSmokeTest, 30 * 1000) }) @@ -364,12 +365,12 @@ loadHttpServer.post('/state/maint', function (req, res, next) { const port = __guard__( Settings.internal != null ? Settings.internal.clsi : undefined, - (x) => x.port + x => x.port ) || 3013 const host = __guard__( Settings.internal != null ? Settings.internal.clsi : undefined, - (x1) => x1.host + x1 => x1.host ) || 'localhost' const loadTcpPort = Settings.internal.load_balancer_agent.load_port @@ -381,12 +382,12 @@ if (!module.parent) { // handle uncaught exceptions when running in production if (Settings.catchErrors) { process.removeAllListeners('uncaughtException') - process.on('uncaughtException', (error) => + process.on('uncaughtException', error => logger.error({ err: error }, 'uncaughtException') ) } - app.listen(port, host, (error) => { + app.listen(port, host, error => { if (error) { logger.fatal({ error }, `Error starting CLSI on ${host}:${port}`) } else { diff --git a/app/js/CompileController.js b/app/js/CompileController.js index acb6626a..488d8181 100644 --- a/app/js/CompileController.js +++ b/app/js/CompileController.js @@ -47,96 +47,94 @@ module.exports = CompileController = { if (error != null) { return next(error) } - return CompileManager.doCompileWithLock(request, function ( - error, - outputFiles, - stats, - timings - ) { - let code, status - if (outputFiles == null) { - outputFiles = [] - } - if (error instanceof Errors.AlreadyCompilingError) { - code = 423 // Http 423 Locked - status = 'compile-in-progress' - } else if (error instanceof Errors.FilesOutOfSyncError) { - code = 409 // Http 409 Conflict - status = 'retry' - } else if (error && error.code === 'EPIPE') { - // docker returns EPIPE when shutting down - code = 503 // send 503 Unavailable response - status = 'unavailable' - } else if (error != null ? error.terminated : undefined) { - status = 'terminated' - } else if (error != null ? error.validate : undefined) { - status = `validation-${error.validate}` - } else if (error != null ? error.timedout : undefined) { - status = 'timedout' - logger.log( - { err: error, project_id: request.project_id }, - 'timeout running compile' - ) - } else if (error != null) { - status = 'error' - code = 500 - logger.warn( - { err: error, project_id: request.project_id }, - 'error running compile' - ) - } else { - let file - status = 'failure' - for (file of Array.from(outputFiles)) { - if (file.path === 'output.pdf' && file.size > 0) { - status = 'success' - } + return CompileManager.doCompileWithLock( + request, + function (error, outputFiles, stats, timings) { + let code, status + if (outputFiles == null) { + outputFiles = [] } - - if (status === 'failure') { + if (error instanceof Errors.AlreadyCompilingError) { + code = 423 // Http 423 Locked + status = 'compile-in-progress' + } else if (error instanceof Errors.FilesOutOfSyncError) { + code = 409 // Http 409 Conflict + status = 'retry' + } else if (error && error.code === 'EPIPE') { + // docker returns EPIPE when shutting down + code = 503 // send 503 Unavailable response + status = 'unavailable' + } else if (error != null ? error.terminated : undefined) { + status = 'terminated' + } else if (error != null ? error.validate : undefined) { + status = `validation-${error.validate}` + } else if (error != null ? error.timedout : undefined) { + status = 'timedout' + logger.log( + { err: error, project_id: request.project_id }, + 'timeout running compile' + ) + } else if (error != null) { + status = 'error' + code = 500 logger.warn( - { project_id: request.project_id, outputFiles }, - 'project failed to compile successfully, no output.pdf generated' + { err: error, project_id: request.project_id }, + 'error running compile' ) - } + } else { + let file + status = 'failure' + for (file of Array.from(outputFiles)) { + if (file.path === 'output.pdf' && file.size > 0) { + status = 'success' + } + } - // log an error if any core files are found - for (file of Array.from(outputFiles)) { - if (file.path === 'core') { - logger.error( - { project_id: request.project_id, req, outputFiles }, - 'core file found in output' + if (status === 'failure') { + logger.warn( + { project_id: request.project_id, outputFiles }, + 'project failed to compile successfully, no output.pdf generated' ) } - } - } - - if (error != null) { - outputFiles = error.outputFiles || [] - } - timer.done() - return res.status(code || 200).send({ - compile: { - status, - error: (error != null ? error.message : undefined) || error, - stats, - timings, - outputFiles: outputFiles.map((file) => { - return { - url: - `${Settings.apis.clsi.url}/project/${request.project_id}` + - (request.user_id != null - ? `/user/${request.user_id}` - : '') + - (file.build != null ? `/build/${file.build}` : '') + - `/output/${file.path}`, - ...file + // log an error if any core files are found + for (file of Array.from(outputFiles)) { + if (file.path === 'core') { + logger.error( + { project_id: request.project_id, req, outputFiles }, + 'core file found in output' + ) } - }) + } } - }) - }) + + if (error != null) { + outputFiles = error.outputFiles || [] + } + + timer.done() + return res.status(code || 200).send({ + compile: { + status, + error: (error != null ? error.message : undefined) || error, + stats, + timings, + outputFiles: outputFiles.map(file => { + return { + url: + `${Settings.apis.clsi.url}/project/${request.project_id}` + + (request.user_id != null + ? `/user/${request.user_id}` + : '') + + (file.build != null ? `/build/${file.build}` : '') + + `/output/${file.path}`, + ...file, + } + }), + }, + }) + } + ) } ) }) @@ -195,7 +193,7 @@ module.exports = CompileController = { return next(error) } return res.json({ - pdf: pdfPositions + pdf: pdfPositions, }) } ) @@ -227,7 +225,7 @@ module.exports = CompileController = { return next(error) } return res.json({ - code: codePositions + code: codePositions, }) } ) @@ -246,17 +244,20 @@ module.exports = CompileController = { } logger.log({ image, file, project_id }, 'word count request') - return CompileManager.wordcount(project_id, user_id, file, image, function ( - error, - result - ) { - if (error != null) { - return next(error) + return CompileManager.wordcount( + project_id, + user_id, + file, + image, + function (error, result) { + if (error != null) { + return next(error) + } + return res.json({ + texcount: result, + }) } - return res.json({ - texcount: result - }) - }) + ) }, status(req, res, next) { @@ -264,5 +265,5 @@ module.exports = CompileController = { next = function (error) {} } return res.send('OK') - } + }, } diff --git a/app/js/CompileManager.js b/app/js/CompileManager.js index f56bfafa..07a9033b 100644 --- a/app/js/CompileManager.js +++ b/app/js/CompileManager.js @@ -65,7 +65,7 @@ module.exports = CompileManager = { } return LockManager.runWithLock( lockFile, - (releaseLock) => CompileManager.doCompile(request, releaseLock), + releaseLock => CompileManager.doCompile(request, releaseLock), callback ) }) @@ -84,264 +84,266 @@ module.exports = CompileManager = { { project_id: request.project_id, user_id: request.user_id }, 'syncing resources to disk' ) - return ResourceWriter.syncResourcesToDisk(request, compileDir, function ( - error, - resourceList - ) { - // NOTE: resourceList is insecure, it should only be used to exclude files from the output list - if (error != null && error instanceof Errors.FilesOutOfSyncError) { - logger.warn( - { project_id: request.project_id, user_id: request.user_id }, - 'files out of sync, please retry' - ) - return callback(error) - } else if (error != null) { - logger.err( + return ResourceWriter.syncResourcesToDisk( + request, + compileDir, + function (error, resourceList) { + // NOTE: resourceList is insecure, it should only be used to exclude files from the output list + if (error != null && error instanceof Errors.FilesOutOfSyncError) { + logger.warn( + { project_id: request.project_id, user_id: request.user_id }, + 'files out of sync, please retry' + ) + return callback(error) + } else if (error != null) { + logger.err( + { + err: error, + project_id: request.project_id, + user_id: request.user_id, + }, + 'error writing resources to disk' + ) + return callback(error) + } + logger.log( { - err: error, project_id: request.project_id, - user_id: request.user_id + user_id: request.user_id, + time_taken: Date.now() - timer.start, }, - 'error writing resources to disk' + 'written files to disk' ) - return callback(error) - } - logger.log( - { - project_id: request.project_id, - user_id: request.user_id, - time_taken: Date.now() - timer.start - }, - 'written files to disk' - ) - const syncStage = timer.done() + const syncStage = timer.done() - const injectDraftModeIfRequired = function (callback) { - if (request.draft) { - return DraftModeManager.injectDraftMode( - Path.join(compileDir, request.rootResourcePath), - callback - ) - } else { - return callback() - } - } - - const createTikzFileIfRequired = (callback) => - TikzManager.checkMainFile( - compileDir, - request.rootResourcePath, - resourceList, - function (error, needsMainFile) { - if (error != null) { - return callback(error) - } - if (needsMainFile) { - return TikzManager.injectOutputFile( - compileDir, - request.rootResourcePath, - callback - ) - } else { - return callback() - } + const injectDraftModeIfRequired = function (callback) { + if (request.draft) { + return DraftModeManager.injectDraftMode( + Path.join(compileDir, request.rootResourcePath), + callback + ) + } else { + return callback() } - ) - // set up environment variables for chktex - const env = {} - if (Settings.texliveOpenoutAny && Settings.texliveOpenoutAny !== '') { - // override default texlive openout_any environment variable - env.openout_any = Settings.texliveOpenoutAny - } - // only run chktex on LaTeX files (not knitr .Rtex files or any others) - const isLaTeXFile = - request.rootResourcePath != null - ? request.rootResourcePath.match(/\.tex$/i) - : undefined - if (request.check != null && isLaTeXFile) { - env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16' - env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000' - if (request.check === 'error') { - env.CHKTEX_EXIT_ON_ERROR = 1 - } - if (request.check === 'validate') { - env.CHKTEX_VALIDATE = 1 } - } - // apply a series of file modifications/creations for draft mode and tikz - return async.series( - [injectDraftModeIfRequired, createTikzFileIfRequired], - function (error) { - if (error != null) { - return callback(error) - } - timer = new Metrics.Timer('run-compile') - // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) - let tag = - __guard__( - __guard__( - request.imageName != null - ? request.imageName.match(/:(.*)/) - : undefined, - (x1) => x1[1] - ), - (x) => x.replace(/\./g, '-') - ) || 'default' - if (!request.project_id.match(/^[0-9a-f]{24}$/)) { - tag = 'other' - } // exclude smoke test - Metrics.inc('compiles') - Metrics.inc(`compiles-with-image.${tag}`) - const compileName = getCompileName( - request.project_id, - request.user_id - ) - return LatexRunner.runLatex( - compileName, - { - directory: compileDir, - mainFile: request.rootResourcePath, - compiler: request.compiler, - timeout: request.timeout, - image: request.imageName, - flags: request.flags, - environment: env, - compileGroup: request.compileGroup - }, - function (error, output, stats, timings) { - // request was for validation only - let metric_key, metric_value - if (request.check === 'validate') { - const result = (error != null ? error.code : undefined) - ? 'fail' - : 'pass' - error = new Error('validation') - error.validate = result - } - // request was for compile, and failed on validation - if ( - request.check === 'error' && - (error != null ? error.message : undefined) === 'exited' - ) { - error = new Error('compilation') - error.validate = 'fail' - } - // compile was killed by user, was a validation, or a compile which failed validation - if ( - (error != null ? error.terminated : undefined) || - (error != null ? error.validate : undefined) || - (error != null ? error.timedout : undefined) - ) { - OutputFileFinder.findOutputFiles( - resourceList, - compileDir, - function (err, outputFiles) { - if (err != null) { - return callback(err) - } - error.outputFiles = outputFiles // return output files so user can check logs - return callback(error) - } - ) - return - } - // compile completed normally + const createTikzFileIfRequired = callback => + TikzManager.checkMainFile( + compileDir, + request.rootResourcePath, + resourceList, + function (error, needsMainFile) { if (error != null) { return callback(error) } - Metrics.inc('compiles-succeeded') - stats = stats || {} - const object = stats || {} - for (metric_key in object) { - metric_value = object[metric_key] - Metrics.count(metric_key, metric_value) - } - timings = timings || {} - const object1 = timings || {} - for (metric_key in object1) { - metric_value = object1[metric_key] - Metrics.timing(metric_key, metric_value) - } - const loadavg = - typeof os.loadavg === 'function' ? os.loadavg() : undefined - if (loadavg != null) { - Metrics.gauge('load-avg', loadavg[0]) - } - const ts = timer.done() - logger.log( - { - project_id: request.project_id, - user_id: request.user_id, - time_taken: ts, - stats, - timings, - loadavg - }, - 'done compile' - ) - if ((stats != null ? stats['latex-runs'] : undefined) > 0) { - Metrics.timing('run-compile-per-pass', ts / stats['latex-runs']) - } - if ( - (stats != null ? stats['latex-runs'] : undefined) > 0 && - (timings != null ? timings['cpu-time'] : undefined) > 0 - ) { - Metrics.timing( - 'run-compile-cpu-time-per-pass', - timings['cpu-time'] / stats['latex-runs'] + if (needsMainFile) { + return TikzManager.injectOutputFile( + compileDir, + request.rootResourcePath, + callback ) + } else { + return callback() } - // Emit compile time. - timings.compile = ts - - timer = new Metrics.Timer('process-output-files') + } + ) + // set up environment variables for chktex + const env = {} + if (Settings.texliveOpenoutAny && Settings.texliveOpenoutAny !== '') { + // override default texlive openout_any environment variable + env.openout_any = Settings.texliveOpenoutAny + } + // only run chktex on LaTeX files (not knitr .Rtex files or any others) + const isLaTeXFile = + request.rootResourcePath != null + ? request.rootResourcePath.match(/\.tex$/i) + : undefined + if (request.check != null && isLaTeXFile) { + env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16' + env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000' + if (request.check === 'error') { + env.CHKTEX_EXIT_ON_ERROR = 1 + } + if (request.check === 'validate') { + env.CHKTEX_VALIDATE = 1 + } + } - return OutputFileFinder.findOutputFiles( - resourceList, - compileDir, - function (error, outputFiles) { - if (error != null) { - return callback(error) - } - return OutputCacheManager.saveOutputFiles( - { request, stats, timings }, - outputFiles, + // apply a series of file modifications/creations for draft mode and tikz + return async.series( + [injectDraftModeIfRequired, createTikzFileIfRequired], + function (error) { + if (error != null) { + return callback(error) + } + timer = new Metrics.Timer('run-compile') + // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) + let tag = + __guard__( + __guard__( + request.imageName != null + ? request.imageName.match(/:(.*)/) + : undefined, + x1 => x1[1] + ), + x => x.replace(/\./g, '-') + ) || 'default' + if (!request.project_id.match(/^[0-9a-f]{24}$/)) { + tag = 'other' + } // exclude smoke test + Metrics.inc('compiles') + Metrics.inc(`compiles-with-image.${tag}`) + const compileName = getCompileName( + request.project_id, + request.user_id + ) + return LatexRunner.runLatex( + compileName, + { + directory: compileDir, + mainFile: request.rootResourcePath, + compiler: request.compiler, + timeout: request.timeout, + image: request.imageName, + flags: request.flags, + environment: env, + compileGroup: request.compileGroup, + }, + function (error, output, stats, timings) { + // request was for validation only + let metric_key, metric_value + if (request.check === 'validate') { + const result = (error != null ? error.code : undefined) + ? 'fail' + : 'pass' + error = new Error('validation') + error.validate = result + } + // request was for compile, and failed on validation + if ( + request.check === 'error' && + (error != null ? error.message : undefined) === 'exited' + ) { + error = new Error('compilation') + error.validate = 'fail' + } + // compile was killed by user, was a validation, or a compile which failed validation + if ( + (error != null ? error.terminated : undefined) || + (error != null ? error.validate : undefined) || + (error != null ? error.timedout : undefined) + ) { + OutputFileFinder.findOutputFiles( + resourceList, compileDir, - outputDir, - (err, newOutputFiles) => { - if (err) { - const { - project_id: projectId, - user_id: userId - } = request - logger.err( - { projectId, userId, err }, - 'failed to save output files' - ) + function (err, outputFiles) { + if (err != null) { + return callback(err) } - - const outputStage = timer.done() - timings.sync = syncStage - timings.output = outputStage - - // Emit e2e compile time. - timings.compileE2E = timerE2E.done() - - if (stats['pdf-size']) { - emitPdfStats(stats, timings) - } - - callback(null, newOutputFiles, stats, timings) + error.outputFiles = outputFiles // return output files so user can check logs + return callback(error) } ) + return } - ) - } - ) - } - ) - }) + // compile completed normally + if (error != null) { + return callback(error) + } + Metrics.inc('compiles-succeeded') + stats = stats || {} + const object = stats || {} + for (metric_key in object) { + metric_value = object[metric_key] + Metrics.count(metric_key, metric_value) + } + timings = timings || {} + const object1 = timings || {} + for (metric_key in object1) { + metric_value = object1[metric_key] + Metrics.timing(metric_key, metric_value) + } + const loadavg = + typeof os.loadavg === 'function' ? os.loadavg() : undefined + if (loadavg != null) { + Metrics.gauge('load-avg', loadavg[0]) + } + const ts = timer.done() + logger.log( + { + project_id: request.project_id, + user_id: request.user_id, + time_taken: ts, + stats, + timings, + loadavg, + }, + 'done compile' + ) + if ((stats != null ? stats['latex-runs'] : undefined) > 0) { + Metrics.timing( + 'run-compile-per-pass', + ts / stats['latex-runs'] + ) + } + if ( + (stats != null ? stats['latex-runs'] : undefined) > 0 && + (timings != null ? timings['cpu-time'] : undefined) > 0 + ) { + Metrics.timing( + 'run-compile-cpu-time-per-pass', + timings['cpu-time'] / stats['latex-runs'] + ) + } + // Emit compile time. + timings.compile = ts + + timer = new Metrics.Timer('process-output-files') + + return OutputFileFinder.findOutputFiles( + resourceList, + compileDir, + function (error, outputFiles) { + if (error != null) { + return callback(error) + } + return OutputCacheManager.saveOutputFiles( + { request, stats, timings }, + outputFiles, + compileDir, + outputDir, + (err, newOutputFiles) => { + if (err) { + const { project_id: projectId, user_id: userId } = + request + logger.err( + { projectId, userId, err }, + 'failed to save output files' + ) + } + + const outputStage = timer.done() + timings.sync = syncStage + timings.output = outputStage + + // Emit e2e compile time. + timings.compileE2E = timerE2E.done() + + if (stats['pdf-size']) { + emitPdfStats(stats, timings) + } + + callback(null, newOutputFiles, stats, timings) + } + ) + } + ) + } + ) + } + ) + } + ) }, stopCompile(project_id, user_id, callback) { @@ -377,13 +379,13 @@ module.exports = CompileManager = { '-f', '--', compileDir, - outputDir + outputDir, ]) proc.on('error', callback) let stderr = '' - proc.stderr.setEncoding('utf8').on('data', (chunk) => (stderr += chunk)) + proc.stderr.setEncoding('utf8').on('data', chunk => (stderr += chunk)) return proc.on('close', function (code) { if (code === 0) { @@ -406,7 +408,7 @@ module.exports = CompileManager = { if (err != null) { return callback(err) } - const allDirs = Array.from(files).map((file) => Path.join(root, file)) + const allDirs = Array.from(files).map(file => Path.join(root, file)) return callback(null, allDirs) }) }, @@ -575,7 +577,7 @@ module.exports = CompileManager = { const timeout = 60 * 1000 // increased to allow for large projects const compileName = getCompileName(project_id, user_id) const compileGroup = 'synctex' - CompileManager._checkFileExists(directory, 'output.synctex.gz', (error) => { + CompileManager._checkFileExists(directory, 'output.synctex.gz', error => { if (error) { return callback(error) } @@ -614,7 +616,7 @@ module.exports = CompileManager = { h: parseFloat(h), v: parseFloat(v), height: parseFloat(height), - width: parseFloat(width) + width: parseFloat(width), }) } } @@ -631,7 +633,7 @@ module.exports = CompileManager = { results.push({ file, line: parseInt(line, 10), - column: parseInt(column, 10) + column: parseInt(column, 10), }) } } @@ -649,7 +651,7 @@ module.exports = CompileManager = { '-nocol', '-inc', file_path, - `-out=${file_path}.wc` + `-out=${file_path}.wc`, ] const compileDir = getCompileDir(project_id, user_id) const timeout = 60 * 1000 @@ -711,7 +713,7 @@ module.exports = CompileManager = { mathInline: 0, mathDisplay: 0, errors: 0, - messages: '' + messages: '', } for (const line of Array.from(output.split('\n'))) { const [data, info] = Array.from(line.split(':')) @@ -749,7 +751,7 @@ module.exports = CompileManager = { } } return results - } + }, } function __guard__(value, transform) { diff --git a/app/js/ContentCacheManager.js b/app/js/ContentCacheManager.js index 7dfc40f0..20360570 100644 --- a/app/js/ContentCacheManager.js +++ b/app/js/ContentCacheManager.js @@ -76,14 +76,14 @@ async function update(contentDir, filePath, size, compileTime) { if (bytesRead !== object.size) { throw new OError('could not read full chunk', { object, - bytesRead + bytesRead, }) } const idxObj = buffer.indexOf('obj') if (idxObj > 100) { throw new OError('objectId is too large', { object, - idxObj + idxObj, }) } const objectIdRaw = buffer.subarray(0, idxObj) @@ -95,7 +95,7 @@ async function update(contentDir, filePath, size, compileTime) { objectId: objectIdRaw.toString(), start: object.offset + objectIdRaw.byteLength, end: object.endOffset, - hash + hash, } ranges.push(range) @@ -168,7 +168,7 @@ class HashFileTracker { const statePath = getStatePath(this.contentDir) const blob = JSON.stringify({ hashAge: Array.from(this.hashAge.entries()), - hashSize: Array.from(this.hashSize.entries()) + hashSize: Array.from(this.hashSize.entries()), }) const atomicWrite = statePath + '~' try { @@ -198,7 +198,7 @@ class HashFileTracker { return reclaimedSpace } - await promiseMapWithLimit(10, hashes, async (hash) => { + await promiseMapWithLimit(10, hashes, async hash => { await fs.promises.unlink(Path.join(this.contentDir, hash)) this.hashAge.delete(hash) reclaimedSpace += this.hashSize.get(hash) @@ -251,7 +251,7 @@ function getDeadlineChecker(compileTime) { throw new TimedOutError(stage, { completedStages, lastStage: lastStage.stage, - diffToLastStage: now - lastStage.now + diffToLastStage: now - lastStage.now, }) } completedStages++ @@ -261,13 +261,13 @@ function getDeadlineChecker(compileTime) { function promiseMapWithLimit(concurrency, array, fn) { const limit = pLimit(concurrency) - return Promise.all(array.map((x) => limit(() => fn(x)))) + return Promise.all(array.map(x => limit(() => fn(x)))) } module.exports = { HASH_REGEX: /^[0-9a-f]{64}$/, update: callbackify(update), promises: { - update - } + update, + }, } diff --git a/app/js/ContentCacheMetrics.js b/app/js/ContentCacheMetrics.js index 6b7a33de..3550de70 100644 --- a/app/js/ContentCacheMetrics.js +++ b/app/js/ContentCacheMetrics.js @@ -4,13 +4,13 @@ const os = require('os') let CACHED_LOAD = { expires: -1, - load: [0, 0, 0] + load: [0, 0, 0], } function getSystemLoad() { if (CACHED_LOAD.expires < Date.now()) { CACHED_LOAD = { expires: Date.now() + 10 * 1000, - load: os.loadavg() + load: os.loadavg(), } } return CACHED_LOAD.load @@ -47,7 +47,7 @@ function emitPdfCachingStats(stats, timings) { { stats, timings, - load: getSystemLoad() + load: getSystemLoad(), }, 'slow pdf caching' ) @@ -111,5 +111,5 @@ function emitPdfCachingStats(stats, timings) { } module.exports = { - emitPdfStats + emitPdfStats, } diff --git a/app/js/ContentTypeMapper.js b/app/js/ContentTypeMapper.js index f690bf9d..6301dce4 100644 --- a/app/js/ContentTypeMapper.js +++ b/app/js/ContentTypeMapper.js @@ -34,5 +34,5 @@ module.exports = ContentTypeMapper = { default: return 'application/octet-stream' } - } + }, } diff --git a/app/js/DockerLockManager.js b/app/js/DockerLockManager.js index 0ed09856..d785ee46 100644 --- a/app/js/DockerLockManager.js +++ b/app/js/DockerLockManager.js @@ -109,5 +109,5 @@ module.exports = LockManager = { }) ) }) - } + }, } diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 2d0810d1..5de05868 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -52,7 +52,7 @@ const DockerRunner = { const volumes = { [directory]: '/compile' } - command = command.map((arg) => + command = command.map(arg => arg.toString().replace('$COMPILE_DIR', '/compile') ) if (image == null) { @@ -96,7 +96,7 @@ const DockerRunner = { { err: error, projectId }, 'error running container so destroying and retrying' ) - DockerRunner.destroyContainer(name, null, true, (error) => { + DockerRunner.destroyContainer(name, null, true, error => { if (error != null) { return callback(error) } @@ -120,7 +120,7 @@ const DockerRunner = { kill(containerId, callback) { logger.log({ containerId }, 'sending kill signal to container') const container = dockerode.getContainer(containerId) - container.kill((error) => { + container.kill(error => { if ( error != null && error.message != null && @@ -250,12 +250,12 @@ const DockerRunner = { { Name: 'cpu', Soft: timeoutInSeconds + 5, - Hard: timeoutInSeconds + 10 - } + Hard: timeoutInSeconds + 10, + }, ], CapDrop: 'ALL', - SecurityOpt: ['no-new-privileges'] - } + SecurityOpt: ['no-new-privileges'], + }, } if (Settings.path != null && Settings.path.synctexBinHostPath != null) { @@ -303,12 +303,12 @@ const DockerRunner = { startContainer(options, volumes, attachStreamHandler, callback) { LockManager.runWithLock( options.name, - (releaseLock) => + releaseLock => // Check that volumes exist before starting the container. // When a container is started with volume pointing to a // non-existent directory then docker creates the directory but // with root ownership. - DockerRunner._checkVolumes(options, volumes, (err) => { + DockerRunner._checkVolumes(options, volumes, err => { if (err != null) { return releaseLock(err) } @@ -343,7 +343,7 @@ const DockerRunner = { }) const jobs = [] for (const vol in volumes) { - jobs.push((cb) => checkVolume(vol, cb)) + jobs.push(cb => checkVolume(vol, cb)) } async.series(jobs, callback) }, @@ -368,11 +368,11 @@ const DockerRunner = { DockerRunner.attachToContainer( options.name, attachStreamHandler, - (error) => { + error => { if (error != null) { return callback(error) } - container.start((error) => { + container.start(error => { if (error != null && error.statusCode !== 304) { callback(error) } else { @@ -430,14 +430,14 @@ const DockerRunner = { { containerId, length: this.data.length, - maxLen: MAX_OUTPUT + maxLen: MAX_OUTPUT, }, `${name} exceeds max size` ) this.data += `(...truncated at ${MAX_OUTPUT} chars...)` this.overflowed = true } - } + }, // kill container if too much output // docker.containers.kill(containerId, () ->) } @@ -448,7 +448,7 @@ const DockerRunner = { container.modem.demuxStream(stream, stdout, stderr) - stream.on('error', (err) => + stream.on('error', err => logger.error( { err, containerId }, 'error reading from container stream' @@ -470,7 +470,7 @@ const DockerRunner = { const timeoutId = setTimeout(() => { timedOut = true logger.log({ containerId }, 'timeout reached, killing container') - container.kill((err) => { + container.kill(err => { logger.warn({ err, containerId }, 'failed to kill container') }) }, timeout) @@ -507,7 +507,7 @@ const DockerRunner = { // supplied. LockManager.runWithLock( containerName, - (releaseLock) => + releaseLock => DockerRunner._destroyContainer( containerId || containerName, shouldForce, @@ -520,7 +520,7 @@ const DockerRunner = { _destroyContainer(containerId, shouldForce, callback) { logger.log({ containerId }, 'destroying docker container') const container = dockerode.getContainer(containerId) - container.remove({ force: shouldForce === true, v: true }, (error) => { + container.remove({ force: shouldForce === true, v: true }, error => { if (error != null && error.statusCode === 404) { logger.warn( { err: error, containerId }, @@ -567,7 +567,7 @@ const DockerRunner = { // strip the / prefix // the LockManager uses the plain container name const plainName = name.slice(1) - jobs.push((cb) => + jobs.push(cb => DockerRunner.destroyContainer(plainName, id, false, () => cb()) ) } @@ -592,7 +592,7 @@ const DockerRunner = { containerMonitorTimeout = setTimeout(() => { containerMonitorInterval = setInterval( () => - DockerRunner.destroyOldContainers((err) => { + DockerRunner.destroyOldContainers(err => { if (err) { logger.error({ err }, 'failed to destroy old containers') } @@ -611,7 +611,7 @@ const DockerRunner = { clearInterval(containerMonitorInterval) containerMonitorInterval = undefined } - } + }, } DockerRunner.startContainerMonitor() diff --git a/app/js/DraftModeManager.js b/app/js/DraftModeManager.js index 0bdd40f0..9be65e7a 100644 --- a/app/js/DraftModeManager.js +++ b/app/js/DraftModeManager.js @@ -37,7 +37,7 @@ module.exports = DraftModeManager = { { content: content.slice(0, 1024), // \documentclass is normally v near the top modified_content: modified_content.slice(0, 1024), - filename + filename, }, 'injected draft class' ) @@ -53,5 +53,5 @@ module.exports = DraftModeManager = { // Without existing options .replace(/\\documentclass\{/g, '\\documentclass[draft]{') ) - } + }, } diff --git a/app/js/Errors.js b/app/js/Errors.js index 0b168034..6b66c234 100644 --- a/app/js/Errors.js +++ b/app/js/Errors.js @@ -37,5 +37,5 @@ module.exports = Errors = { TimedOutError, NotFoundError, FilesOutOfSyncError, - AlreadyCompilingError + AlreadyCompilingError, } diff --git a/app/js/LatexRunner.js b/app/js/LatexRunner.js index 7cd67a03..7c288cef 100644 --- a/app/js/LatexRunner.js +++ b/app/js/LatexRunner.js @@ -26,7 +26,7 @@ const ProcessTable = {} // table of currently running jobs (pids or docker conta const TIME_V_METRICS = Object.entries({ 'cpu-percent': /Percent of CPU this job got: (\d+)/m, 'cpu-time': /User time.*: (\d+.\d+)/m, - 'sys-time': /System time.*: (\d+.\d+)/m + 'sys-time': /System time.*: (\d+.\d+)/m, }) module.exports = LatexRunner = { @@ -43,7 +43,7 @@ module.exports = LatexRunner = { image, environment, flags, - compileGroup + compileGroup, } = options if (!compiler) { compiler = 'pdflatex' @@ -60,7 +60,7 @@ module.exports = LatexRunner = { mainFile, environment, flags, - compileGroup + compileGroup, }, 'starting compile' ) @@ -102,13 +102,13 @@ module.exports = LatexRunner = { } const runs = __guard__( - __guard__(output != null ? output.stderr : undefined, (x1) => + __guard__(output != null ? output.stderr : undefined, x1 => x1.match(/^Run number \d+ of .*latex/gm) ), - (x) => x.length + x => x.length ) || 0 const failed = - __guard__(output != null ? output.stdout : undefined, (x2) => + __guard__(output != null ? output.stdout : undefined, x2 => x2.match(/^Latexmk: Errors/m) ) != null ? 1 @@ -147,7 +147,7 @@ module.exports = LatexRunner = { // internal method for writing non-empty log files function _writeFile(file, content, cb) { if (content && content.length > 0) { - fs.writeFile(file, content, (err) => { + fs.writeFile(file, content, err => { if (err) { logger.error({ project_id, file }, 'error writing log file') // don't fail on error } @@ -188,7 +188,7 @@ module.exports = LatexRunner = { '-auxdir=$COMPILE_DIR', '-outdir=$COMPILE_DIR', '-synctex=1', - '-interaction=batchmode' + '-interaction=batchmode', ] if (flags) { args = args.concat(flags) @@ -196,7 +196,7 @@ module.exports = LatexRunner = { return ( __guard__( Settings != null ? Settings.clsi : undefined, - (x) => x.latexmkCommandPrefix + x => x.latexmkCommandPrefix ) || [] ).concat(args) }, @@ -204,30 +204,30 @@ module.exports = LatexRunner = { _pdflatexCommand(mainFile, flags) { return LatexRunner._latexmkBaseCommand(flags).concat([ '-pdf', - Path.join('$COMPILE_DIR', mainFile) + Path.join('$COMPILE_DIR', mainFile), ]) }, _latexCommand(mainFile, flags) { return LatexRunner._latexmkBaseCommand(flags).concat([ '-pdfdvi', - Path.join('$COMPILE_DIR', mainFile) + Path.join('$COMPILE_DIR', mainFile), ]) }, _xelatexCommand(mainFile, flags) { return LatexRunner._latexmkBaseCommand(flags).concat([ '-xelatex', - Path.join('$COMPILE_DIR', mainFile) + Path.join('$COMPILE_DIR', mainFile), ]) }, _lualatexCommand(mainFile, flags) { return LatexRunner._latexmkBaseCommand(flags).concat([ '-lualatex', - Path.join('$COMPILE_DIR', mainFile) + Path.join('$COMPILE_DIR', mainFile), ]) - } + }, } function __guard__(value, transform) { diff --git a/app/js/LocalCommandRunner.js b/app/js/LocalCommandRunner.js index d5fd3090..1e4236a5 100644 --- a/app/js/LocalCommandRunner.js +++ b/app/js/LocalCommandRunner.js @@ -37,7 +37,7 @@ module.exports = CommandRunner = { } else { callback = _.once(callback) } - command = Array.from(command).map((arg) => + command = Array.from(command).map(arg => arg.toString().replace('$COMPILE_DIR', directory) ) logger.log({ project_id, command, directory }, 'running command') @@ -58,7 +58,7 @@ module.exports = CommandRunner = { const proc = spawn(command[0], command.slice(1), { cwd: directory, env }) let stdout = '' - proc.stdout.setEncoding('utf8').on('data', (data) => (stdout += data)) + proc.stdout.setEncoding('utf8').on('data', data => (stdout += data)) proc.on('error', function (err) { logger.err( @@ -99,5 +99,5 @@ module.exports = CommandRunner = { return callback(err) } return callback() - } + }, } diff --git a/app/js/LockManager.js b/app/js/LockManager.js index a3bdf1bc..2dc09b33 100644 --- a/app/js/LockManager.js +++ b/app/js/LockManager.js @@ -30,7 +30,7 @@ module.exports = LockManager = { const lockOpts = { wait: this.MAX_LOCK_WAIT_TIME, pollPeriod: this.LOCK_TEST_INTERVAL, - stale: this.LOCK_STALE + stale: this.LOCK_STALE, } return Lockfile.lock(path, lockOpts, function (error) { if ((error != null ? error.code : undefined) === 'EEXIST') { @@ -48,7 +48,7 @@ module.exports = LockManager = { statDir, statDirErr, readdirErr, - readdirDir + readdirDir, }, 'unable to get lock' ) @@ -68,5 +68,5 @@ module.exports = LockManager = { ) } }) - } + }, } diff --git a/app/js/OutputCacheManager.js b/app/js/OutputCacheManager.js index 682b85aa..af85f496 100644 --- a/app/js/OutputCacheManager.js +++ b/app/js/OutputCacheManager.js @@ -180,38 +180,42 @@ module.exports = OutputCacheManager = { const newFile = _.clone(file) const [src, dst] = Array.from([ Path.join(compileDir, file.path), - Path.join(cacheDir, file.path) + Path.join(cacheDir, file.path), ]) - return OutputCacheManager._checkFileIsSafe(src, function ( - err, - isSafe - ) { - if (err != null) { - return cb(err) - } - if (!isSafe) { - return cb() - } - return OutputCacheManager._checkIfShouldCopy(src, function ( - err, - shouldCopy - ) { + return OutputCacheManager._checkFileIsSafe( + src, + function (err, isSafe) { if (err != null) { return cb(err) } - if (!shouldCopy) { + if (!isSafe) { return cb() } - return OutputCacheManager._copyFile(src, dst, function (err) { - if (err != null) { - return cb(err) + return OutputCacheManager._checkIfShouldCopy( + src, + function (err, shouldCopy) { + if (err != null) { + return cb(err) + } + if (!shouldCopy) { + return cb() + } + return OutputCacheManager._copyFile( + src, + dst, + function (err) { + if (err != null) { + return cb(err) + } + newFile.build = buildId // attach a build id if we cached the file + results.push(newFile) + return cb() + } + ) } - newFile.build = buildId // attach a build id if we cached the file - results.push(newFile) - return cb() - }) - }) - }) + ) + } + ) }, function (err) { if (err != null) { @@ -232,7 +236,7 @@ module.exports = OutputCacheManager = { // let file expiry run in the background, expire all previous files if per-user return OutputCacheManager.expireOutputFiles(cacheRoot, { keep: buildId, - limit: perUser ? 1 : null + limit: perUser ? 1 : null, }) } } @@ -242,7 +246,7 @@ module.exports = OutputCacheManager = { }, collectOutputPdfSize(outputFiles, outputDir, stats, callback) { - const outputFile = outputFiles.find((x) => x.path === 'output.pdf') + const outputFile = outputFiles.find(x => x.path === 'output.pdf') if (!outputFile) return callback(null, outputFiles) const outputFilePath = Path.join( outputDir, @@ -269,7 +273,7 @@ module.exports = OutputCacheManager = { OutputCacheManager.ensureContentDir(cacheRoot, function (err, contentDir) { if (err) return callback(err, outputFiles) - const outputFile = outputFiles.find((x) => x.path === 'output.pdf') + const outputFile = outputFiles.find(x => x.path === 'output.pdf') if (outputFile) { // possibly we should copy the file from the build dir here const outputFilePath = Path.join( @@ -331,7 +335,7 @@ module.exports = OutputCacheManager = { } fs.readdir(contentRoot, function (err, results) { const dirs = results.sort() - const contentId = dirs.find((dir) => + const contentId = dirs.find(dir => OutputCacheManager.BUILD_REGEX.test(dir) ) if (contentId) { @@ -374,31 +378,31 @@ module.exports = OutputCacheManager = { function (file, cb) { const [src, dst] = Array.from([ Path.join(compileDir, file.path), - Path.join(archiveDir, file.path) + Path.join(archiveDir, file.path), ]) - return OutputCacheManager._checkFileIsSafe(src, function ( - err, - isSafe - ) { - if (err != null) { - return cb(err) - } - if (!isSafe) { - return cb() - } - return OutputCacheManager._checkIfShouldArchive(src, function ( - err, - shouldArchive - ) { + return OutputCacheManager._checkFileIsSafe( + src, + function (err, isSafe) { if (err != null) { return cb(err) } - if (!shouldArchive) { + if (!isSafe) { return cb() } - return OutputCacheManager._copyFile(src, dst, cb) - }) - }) + return OutputCacheManager._checkIfShouldArchive( + src, + function (err, shouldArchive) { + if (err != null) { + return cb(err) + } + if (!shouldArchive) { + return cb() + } + return OutputCacheManager._copyFile(src, dst, cb) + } + ) + } + ) }, callback ) @@ -440,7 +444,7 @@ module.exports = OutputCacheManager = { // we can get the build time from the first part of the directory name DDDD-RRRR // DDDD is date and RRRR is random bytes const dirTime = parseInt( - __guard__(dir.split('-'), (x) => x[0]), + __guard__(dir.split('-'), x => x[0]), 16 ) const age = currentTime - dirTime @@ -549,7 +553,7 @@ module.exports = OutputCacheManager = { return callback(null, true) } return callback(null, false) - } + }, } function __guard__(value, transform) { diff --git a/app/js/OutputFileFinder.js b/app/js/OutputFileFinder.js index d9d24996..9088215d 100644 --- a/app/js/OutputFileFinder.js +++ b/app/js/OutputFileFinder.js @@ -6,9 +6,7 @@ const logger = require('logger-sharelatex') module.exports = OutputFileFinder = { findOutputFiles(resources, directory, callback) { - const incomingResources = new Set( - resources.map((resource) => resource.path) - ) + const incomingResources = new Set(resources.map(resource => resource.path)) OutputFileFinder._getAllFiles(directory, function (error, allFiles) { if (allFiles == null) { @@ -23,7 +21,7 @@ module.exports = OutputFileFinder = { if (!incomingResources.has(file)) { outputFiles.push({ path: file, - type: Path.extname(file).replace(/^\./, '') || undefined + type: Path.extname(file).replace(/^\./, '') || undefined, }) } } @@ -42,7 +40,7 @@ module.exports = OutputFileFinder = { '.archive', '-o', '-name', - '.project-*' + '.project-*', ] const args = [ directory, @@ -53,13 +51,13 @@ module.exports = OutputFileFinder = { '-o', '-type', 'f', - '-print' + '-print', ] logger.log({ args }, 'running find command') const proc = spawn('find', args) let stdout = '' - proc.stdout.setEncoding('utf8').on('data', (chunk) => (stdout += chunk)) + proc.stdout.setEncoding('utf8').on('data', chunk => (stdout += chunk)) proc.on('error', callback) proc.on('close', function (code) { if (code !== 0) { @@ -76,5 +74,5 @@ module.exports = OutputFileFinder = { }) callback(null, fileList) }) - } + }, } diff --git a/app/js/OutputFileOptimiser.js b/app/js/OutputFileOptimiser.js index 0b835a42..979697b0 100644 --- a/app/js/OutputFileOptimiser.js +++ b/app/js/OutputFileOptimiser.js @@ -29,15 +29,15 @@ module.exports = OutputFileOptimiser = { callback = function (error) {} } if (src.match(/\/output\.pdf$/)) { - return OutputFileOptimiser.checkIfPDFIsOptimised(src, function ( - err, - isOptimised - ) { - if (err != null || isOptimised) { - return callback(null) + return OutputFileOptimiser.checkIfPDFIsOptimised( + src, + function (err, isOptimised) { + if (err != null || isOptimised) { + return callback(null) + } + return OutputFileOptimiser.optimisePDF(src, dst, callback) } - return OutputFileOptimiser.optimisePDF(src, dst, callback) - }) + ) } else { return callback(null) } @@ -77,7 +77,7 @@ module.exports = OutputFileOptimiser = { const timer = new Metrics.Timer('qpdf') const proc = spawn('qpdf', args) let stdout = '' - proc.stdout.setEncoding('utf8').on('data', (chunk) => (stdout += chunk)) + proc.stdout.setEncoding('utf8').on('data', chunk => (stdout += chunk)) callback = _.once(callback) // avoid double call back for error and close event proc.on('error', function (err) { logger.warn({ err, args }, 'qpdf failed') @@ -99,5 +99,5 @@ module.exports = OutputFileOptimiser = { return callback(null) }) }) - } // ignore the error + }, // ignore the error } diff --git a/app/js/ProjectPersistenceManager.js b/app/js/ProjectPersistenceManager.js index 814f7e99..c4abb69b 100644 --- a/app/js/ProjectPersistenceManager.js +++ b/app/js/ProjectPersistenceManager.js @@ -28,7 +28,7 @@ async function refreshExpiryTimeout() { const paths = [ Settings.path.compilesDir, Settings.path.outputDir, - Settings.path.clsiCacheDir + Settings.path.clsiCacheDir, ] for (const path of paths) { try { @@ -40,7 +40,7 @@ async function refreshExpiryTimeout() { logger.warn( { stats, - newExpiryTimeoutInDays: (lowerExpiry / oneDay).toFixed(2) + newExpiryTimeoutInDays: (lowerExpiry / oneDay).toFixed(2), }, 'disk running low on space, modifying EXPIRY_TIMEOUT' ) @@ -57,7 +57,7 @@ module.exports = ProjectPersistenceManager = { EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5, promises: { - refreshExpiryTimeout + refreshExpiryTimeout, }, refreshExpiryTimeout: callbackify(refreshExpiryTimeout), @@ -66,7 +66,7 @@ module.exports = ProjectPersistenceManager = { callback = function (error) {} } const timer = new Metrics.Timer('db-bump-last-accessed') - const job = (cb) => + const job = cb => db.Project.findOrCreate({ where: { project_id } }) .spread((project, created) => project @@ -75,7 +75,7 @@ module.exports = ProjectPersistenceManager = { .error(cb) ) .error(cb) - dbQueue.queue.push(job, (error) => { + dbQueue.queue.push(job, error => { timer.done() callback(error) }) @@ -93,16 +93,19 @@ module.exports = ProjectPersistenceManager = { return callback(error) } logger.log({ project_ids }, 'clearing expired projects') - const jobs = Array.from(project_ids || []).map((project_id) => - ((project_id) => (callback) => - ProjectPersistenceManager.clearProjectFromCache(project_id, function ( - err - ) { - if (err != null) { - logger.error({ err, project_id }, 'error clearing project') - } - return callback() - }))(project_id) + const jobs = Array.from(project_ids || []).map(project_id => + ( + project_id => callback => + ProjectPersistenceManager.clearProjectFromCache( + project_id, + function (err) { + if (err != null) { + logger.error({ err, project_id }, 'error clearing project') + } + return callback() + } + ) + )(project_id) ) return async.series(jobs, function (error) { if (error != null) { @@ -110,7 +113,7 @@ module.exports = ProjectPersistenceManager = { } return CompileManager.clearExpiredProjects( ProjectPersistenceManager.EXPIRY_TIMEOUT, - (error) => callback() + error => callback() ) }) }) @@ -167,7 +170,7 @@ module.exports = ProjectPersistenceManager = { callback = function (error) {} } logger.log({ project_id }, 'clearing project from database') - const job = (cb) => + const job = cb => db.Project.destroy({ where: { project_id } }) .then(() => cb()) .error(cb) @@ -185,17 +188,17 @@ module.exports = ProjectPersistenceManager = { const q = {} q[db.op.lt] = keepProjectsFrom return db.Project.findAll({ where: { lastAccessed: q } }) - .then((projects) => + .then(projects => cb( null, - projects.map((project) => project.project_id) + projects.map(project => project.project_id) ) ) .error(cb) } return dbQueue.queue.push(job, callback) - } + }, } logger.log( diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index 702cf559..cb097f64 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -47,7 +47,7 @@ module.exports = RequestParser = { { validValues: this.VALID_COMPILERS, default: 'pdflatex', - type: 'string' + type: 'string', } ) response.enablePdfCaching = this._parseAttribute( @@ -55,7 +55,7 @@ module.exports = RequestParser = { compile.options.enablePdfCaching, { default: false, - type: 'boolean' + type: 'boolean', } ) response.timeout = this._parseAttribute( @@ -63,7 +63,7 @@ module.exports = RequestParser = { compile.options.timeout, { default: RequestParser.MAX_TIMEOUT, - type: 'number' + type: 'number', } ) response.imageName = this._parseAttribute( @@ -74,19 +74,19 @@ module.exports = RequestParser = { validValues: settings.clsi && settings.clsi.docker && - settings.clsi.docker.allowedImages + settings.clsi.docker.allowedImages, } ) response.draft = this._parseAttribute('draft', compile.options.draft, { default: false, - type: 'boolean' + type: 'boolean', }) response.check = this._parseAttribute('check', compile.options.check, { - type: 'string' + type: 'string', }) response.flags = this._parseAttribute('flags', compile.options.flags, { default: [], - type: 'object' + type: 'object', }) if (settings.allowedCompileGroups) { response.compileGroup = this._parseAttribute( @@ -95,7 +95,7 @@ module.exports = RequestParser = { { validValues: settings.allowedCompileGroups, default: '', - type: 'string' + type: 'string', } ) } @@ -107,7 +107,7 @@ module.exports = RequestParser = { compile.options.syncType, { validValues: ['full', 'incremental'], - type: 'string' + type: 'string', } ) @@ -144,13 +144,12 @@ module.exports = RequestParser = { compile.rootResourcePath, { default: 'main.tex', - type: 'string' + type: 'string', } ) const originalRootResourcePath = rootResourcePath - const sanitizedRootResourcePath = RequestParser._sanitizePath( - rootResourcePath - ) + const sanitizedRootResourcePath = + RequestParser._sanitizePath(rootResourcePath) response.rootResourcePath = RequestParser._checkPath( sanitizedRootResourcePath ) @@ -195,7 +194,7 @@ module.exports = RequestParser = { path: resource.path, modified, url: resource.url, - content: resource.content + content: resource.content, } }, @@ -237,5 +236,5 @@ module.exports = RequestParser = { } } return path - } + }, } diff --git a/app/js/ResourceStateManager.js b/app/js/ResourceStateManager.js index e36f19ed..7ae3557d 100644 --- a/app/js/ResourceStateManager.js +++ b/app/js/ResourceStateManager.js @@ -36,7 +36,7 @@ module.exports = { }) } else { logger.log({ state, basePath }, 'writing sync state') - const resourceList = resources.map((resource) => resource.path) + const resourceList = resources.map(resource => resource.path) fs.writeFile( stateFile, [...resourceList, `stateHash:${state}`].join('\n'), @@ -48,43 +48,46 @@ module.exports = { checkProjectStateMatches(state, basePath, callback) { const stateFile = Path.join(basePath, this.SYNC_STATE_FILE) const size = this.SYNC_STATE_MAX_SIZE - SafeReader.readFile(stateFile, size, 'utf8', function ( - err, - result, - bytesRead - ) { - if (err) { - return callback(err) - } - if (bytesRead === size) { - logger.error( - { file: stateFile, size, bytesRead }, - 'project state file truncated' - ) - } - const array = result ? result.toString().split('\n') : [] - const adjustedLength = Math.max(array.length, 1) - const resourceList = array.slice(0, adjustedLength - 1) - const oldState = array[adjustedLength - 1] - const newState = `stateHash:${state}` - logger.log( - { state, oldState, basePath, stateMatches: newState === oldState }, - 'checking sync state' - ) - if (newState !== oldState) { - return callback( - new Errors.FilesOutOfSyncError('invalid state for incremental update') + SafeReader.readFile( + stateFile, + size, + 'utf8', + function (err, result, bytesRead) { + if (err) { + return callback(err) + } + if (bytesRead === size) { + logger.error( + { file: stateFile, size, bytesRead }, + 'project state file truncated' + ) + } + const array = result ? result.toString().split('\n') : [] + const adjustedLength = Math.max(array.length, 1) + const resourceList = array.slice(0, adjustedLength - 1) + const oldState = array[adjustedLength - 1] + const newState = `stateHash:${state}` + logger.log( + { state, oldState, basePath, stateMatches: newState === oldState }, + 'checking sync state' ) - } else { - const resources = resourceList.map((path) => ({ path })) - callback(null, resources) + if (newState !== oldState) { + return callback( + new Errors.FilesOutOfSyncError( + 'invalid state for incremental update' + ) + ) + } else { + const resources = resourceList.map(path => ({ path })) + callback(null, resources) + } } - }) + ) }, checkResourceFiles(resources, allFiles, basePath, callback) { // check the paths are all relative to current directory - const containsRelativePath = (resource) => { + const containsRelativePath = resource => { const dirs = resource.path.split('/') return dirs.indexOf('..') !== -1 } @@ -94,8 +97,8 @@ module.exports = { // check if any of the input files are not present in list of files const seenFiles = new Set(allFiles) const missingFiles = resources - .map((resource) => resource.path) - .filter((path) => !seenFiles.has(path)) + .map(resource => resource.path) + .filter(path => !seenFiles.has(path)) if (missingFiles.length > 0) { logger.err( { missingFiles, basePath, allFiles, resources }, @@ -109,5 +112,5 @@ module.exports = { } else { callback() } - } + }, } diff --git a/app/js/ResourceWriter.js b/app/js/ResourceWriter.js index 25f1b979..d7cdc502 100644 --- a/app/js/ResourceWriter.js +++ b/app/js/ResourceWriter.js @@ -109,13 +109,13 @@ module.exports = ResourceWriter = { if (callback == null) { callback = function (error) {} } - return this._createDirectory(basePath, (error) => { + return this._createDirectory(basePath, error => { if (error != null) { return callback(error) } - const jobs = Array.from(resources).map((resource) => - ((resource) => { - return (callback) => + const jobs = Array.from(resources).map(resource => + (resource => { + return callback => this._writeResourceToDisk(project_id, resource, basePath, callback) })(resource) ) @@ -127,17 +127,17 @@ module.exports = ResourceWriter = { if (callback == null) { callback = function (error) {} } - return this._createDirectory(basePath, (error) => { + return this._createDirectory(basePath, error => { if (error != null) { return callback(error) } - return this._removeExtraneousFiles(resources, basePath, (error) => { + return this._removeExtraneousFiles(resources, basePath, error => { if (error != null) { return callback(error) } - const jobs = Array.from(resources).map((resource) => - ((resource) => { - return (callback) => + const jobs = Array.from(resources).map(resource => + (resource => { + return callback => this._writeResourceToDisk( project_id, resource, @@ -179,86 +179,86 @@ module.exports = ResourceWriter = { return _callback(error, ...Array.from(result)) } - return OutputFileFinder.findOutputFiles(resources, basePath, function ( - error, - outputFiles, - allFiles - ) { - if (error != null) { - return callback(error) - } + return OutputFileFinder.findOutputFiles( + resources, + basePath, + function (error, outputFiles, allFiles) { + if (error != null) { + return callback(error) + } - const jobs = [] - for (const file of Array.from(outputFiles || [])) { - ;(function (file) { - const { path } = file - let should_delete = true - if ( - path.match(/^output\./) || - path.match(/\.aux$/) || - path.match(/^cache\//) - ) { - // knitr cache - should_delete = false - } - if (path.match(/^output-.*/)) { - // Tikz cached figures (default case) - should_delete = false - } - if (path.match(/\.(pdf|dpth|md5)$/)) { - // Tikz cached figures (by extension) - should_delete = false - } - if ( - path.match(/\.(pygtex|pygstyle)$/) || - path.match(/(^|\/)_minted-[^\/]+\//) - ) { - // minted files/directory - should_delete = false - } - if ( - path.match(/\.md\.tex$/) || - path.match(/(^|\/)_markdown_[^\/]+\//) - ) { - // markdown files/directory - should_delete = false - } - if (path.match(/-eps-converted-to\.pdf$/)) { - // Epstopdf generated files - should_delete = false - } - if ( - path === 'output.pdf' || - path === 'output.dvi' || - path === 'output.log' || - path === 'output.xdv' || - path === 'output.stdout' || - path === 'output.stderr' - ) { - should_delete = true - } - if (path === 'output.tex') { - // created by TikzManager if present in output files - should_delete = true - } - if (should_delete) { - return jobs.push((callback) => - ResourceWriter._deleteFileIfNotDirectory( - Path.join(basePath, path), - callback + const jobs = [] + for (const file of Array.from(outputFiles || [])) { + ;(function (file) { + const { path } = file + let should_delete = true + if ( + path.match(/^output\./) || + path.match(/\.aux$/) || + path.match(/^cache\//) + ) { + // knitr cache + should_delete = false + } + if (path.match(/^output-.*/)) { + // Tikz cached figures (default case) + should_delete = false + } + if (path.match(/\.(pdf|dpth|md5)$/)) { + // Tikz cached figures (by extension) + should_delete = false + } + if ( + path.match(/\.(pygtex|pygstyle)$/) || + path.match(/(^|\/)_minted-[^\/]+\//) + ) { + // minted files/directory + should_delete = false + } + if ( + path.match(/\.md\.tex$/) || + path.match(/(^|\/)_markdown_[^\/]+\//) + ) { + // markdown files/directory + should_delete = false + } + if (path.match(/-eps-converted-to\.pdf$/)) { + // Epstopdf generated files + should_delete = false + } + if ( + path === 'output.pdf' || + path === 'output.dvi' || + path === 'output.log' || + path === 'output.xdv' || + path === 'output.stdout' || + path === 'output.stderr' + ) { + should_delete = true + } + if (path === 'output.tex') { + // created by TikzManager if present in output files + should_delete = true + } + if (should_delete) { + return jobs.push(callback => + ResourceWriter._deleteFileIfNotDirectory( + Path.join(basePath, path), + callback + ) ) - ) + } + })(file) + } + + return async.series(jobs, function (error) { + if (error != null) { + return callback(error) } - })(file) + return callback(null, outputFiles, allFiles) + }) } - - return async.series(jobs, function (error) { - if (error != null) { - return callback(error) - } - return callback(null, outputFiles, allFiles) - }) - }) + ) }, _deleteFileIfNotDirectory(path, callback) { @@ -296,48 +296,51 @@ module.exports = ResourceWriter = { if (callback == null) { callback = function (error) {} } - return ResourceWriter.checkPath(basePath, resource.path, function ( - error, - path - ) { - if (error != null) { - return callback(error) - } - return fs.mkdir(Path.dirname(path), { recursive: true }, function ( - error - ) { + return ResourceWriter.checkPath( + basePath, + resource.path, + function (error, path) { if (error != null) { return callback(error) } - // TODO: Don't overwrite file if it hasn't been modified - if (resource.url != null) { - return UrlCache.downloadUrlToFile( - project_id, - resource.url, - path, - resource.modified, - function (err) { - if (err != null) { - logger.err( - { - err, - project_id, - path, - resource_url: resource.url, - modified: resource.modified - }, - 'error downloading file for resources' - ) - Metrics.inc('download-failed') - } - return callback() + return fs.mkdir( + Path.dirname(path), + { recursive: true }, + function (error) { + if (error != null) { + return callback(error) } - ) // try and continue compiling even if http resource can not be downloaded at this time - } else { - fs.writeFile(path, resource.content, callback) - } - }) - }) + // TODO: Don't overwrite file if it hasn't been modified + if (resource.url != null) { + return UrlCache.downloadUrlToFile( + project_id, + resource.url, + path, + resource.modified, + function (err) { + if (err != null) { + logger.err( + { + err, + project_id, + path, + resource_url: resource.url, + modified: resource.modified, + }, + 'error downloading file for resources' + ) + Metrics.inc('download-failed') + } + return callback() + } + ) // try and continue compiling even if http resource can not be downloaded at this time + } else { + fs.writeFile(path, resource.content, callback) + } + } + ) + } + ) }, checkPath(basePath, resourcePath, callback) { @@ -347,5 +350,5 @@ module.exports = ResourceWriter = { } else { return callback(null, path) } - } + }, } diff --git a/app/js/SafeReader.js b/app/js/SafeReader.js index f3188791..760a7725 100644 --- a/app/js/SafeReader.js +++ b/app/js/SafeReader.js @@ -44,17 +44,20 @@ module.exports = SafeReader = { return callback(null, ...Array.from(result)) }) const buff = Buffer.alloc(size) // fills with zeroes by default - return fs.read(fd, buff, 0, buff.length, 0, function ( - err, - bytesRead, - buffer - ) { - if (err != null) { - return callbackWithClose(err) + return fs.read( + fd, + buff, + 0, + buff.length, + 0, + function (err, bytesRead, buffer) { + if (err != null) { + return callbackWithClose(err) + } + const result = buffer.toString(encoding, 0, bytesRead) + return callbackWithClose(null, result, bytesRead) } - const result = buffer.toString(encoding, 0, bytesRead) - return callbackWithClose(null, result, bytesRead) - }) + ) }) - } + }, } diff --git a/app/js/StaticServerForbidSymlinks.js b/app/js/StaticServerForbidSymlinks.js index bcfc5015..f810d2b8 100644 --- a/app/js/StaticServerForbidSymlinks.js +++ b/app/js/StaticServerForbidSymlinks.js @@ -26,7 +26,7 @@ module.exports = ForbidSymlinks = function (staticFn, root, options) { const basePath = Path.resolve(root) return function (req, res, next) { let file, project_id, result - const path = __guard__(url.parse(req.url), (x) => x.pathname) + const path = __guard__(url.parse(req.url), x => x.pathname) // check that the path is of the form /project_id_or_name/path/to/file.log if ((result = path.match(/^\/?([a-zA-Z0-9_-]+)\/(.*)/))) { project_id = result[1] @@ -63,7 +63,7 @@ module.exports = ForbidSymlinks = function (staticFn, root, options) { requestedFsPath, realFsPath, path: req.params[0], - project_id: req.params.project_id + project_id: req.params.project_id, }, 'error checking file access' ) @@ -75,7 +75,7 @@ module.exports = ForbidSymlinks = function (staticFn, root, options) { requestedFsPath, realFsPath, path: req.params[0], - project_id: req.params.project_id + project_id: req.params.project_id, }, 'trying to access a different file (symlink), aborting' ) diff --git a/app/js/TikzManager.js b/app/js/TikzManager.js index 0e39897d..ac62ddac 100644 --- a/app/js/TikzManager.js +++ b/app/js/TikzManager.js @@ -35,63 +35,67 @@ module.exports = TikzManager = { } } // if there's no output.tex, see if we are using tikz/pgf or pstool in the main file - return ResourceWriter.checkPath(compileDir, mainFile, function ( - error, - path - ) { - if (error != null) { - return callback(error) - } - return SafeReader.readFile(path, 65536, 'utf8', function ( - error, - content - ) { + return ResourceWriter.checkPath( + compileDir, + mainFile, + function (error, path) { if (error != null) { return callback(error) } - const usesTikzExternalize = - (content != null - ? content.indexOf('\\tikzexternalize') - : undefined) >= 0 - const usesPsTool = - (content != null ? content.indexOf('{pstool}') : undefined) >= 0 - logger.log( - { compileDir, mainFile, usesTikzExternalize, usesPsTool }, - 'checked for packages needing main file as output.tex' + return SafeReader.readFile( + path, + 65536, + 'utf8', + function (error, content) { + if (error != null) { + return callback(error) + } + const usesTikzExternalize = + (content != null + ? content.indexOf('\\tikzexternalize') + : undefined) >= 0 + const usesPsTool = + (content != null ? content.indexOf('{pstool}') : undefined) >= 0 + logger.log( + { compileDir, mainFile, usesTikzExternalize, usesPsTool }, + 'checked for packages needing main file as output.tex' + ) + const needsMainFile = usesTikzExternalize || usesPsTool + return callback(null, needsMainFile) + } ) - const needsMainFile = usesTikzExternalize || usesPsTool - return callback(null, needsMainFile) - }) - }) + } + ) }, injectOutputFile(compileDir, mainFile, callback) { if (callback == null) { callback = function (error) {} } - return ResourceWriter.checkPath(compileDir, mainFile, function ( - error, - path - ) { - if (error != null) { - return callback(error) - } - return fs.readFile(path, 'utf8', function (error, content) { + return ResourceWriter.checkPath( + compileDir, + mainFile, + function (error, path) { if (error != null) { return callback(error) } - logger.log( - { compileDir, mainFile }, - 'copied file to output.tex as project uses packages which require it' - ) - // use wx flag to ensure that output file does not already exist - return fs.writeFile( - Path.join(compileDir, 'output.tex'), - content, - { flag: 'wx' }, - callback - ) - }) - }) - } + return fs.readFile(path, 'utf8', function (error, content) { + if (error != null) { + return callback(error) + } + logger.log( + { compileDir, mainFile }, + 'copied file to output.tex as project uses packages which require it' + ) + // use wx flag to ensure that output file does not already exist + return fs.writeFile( + Path.join(compileDir, 'output.tex'), + content, + { flag: 'wx' }, + callback + ) + }) + } + ) + }, } diff --git a/app/js/UrlCache.js b/app/js/UrlCache.js index b6378a55..70535b61 100644 --- a/app/js/UrlCache.js +++ b/app/js/UrlCache.js @@ -65,17 +65,19 @@ module.exports = UrlCache = { if (error != null) { return callback(error) } - const jobs = Array.from(urls || []).map((url) => - ((url) => (callback) => - UrlCache._clearUrlFromCache(project_id, url, function (error) { - if (error != null) { - logger.error( - { err: error, project_id, url }, - 'error clearing project URL' - ) - } - return callback() - }))(url) + const jobs = Array.from(urls || []).map(url => + ( + url => callback => + UrlCache._clearUrlFromCache(project_id, url, function (error) { + if (error != null) { + logger.error( + { err: error, project_id, url }, + 'error clearing project URL' + ) + } + return callback() + }) + )(url) ) return async.series(jobs, callback) }) @@ -103,7 +105,7 @@ module.exports = UrlCache = { return UrlFetcher.pipeUrlToFileWithRetry( url, UrlCache._cacheFilePathForUrl(project_id, url), - (error) => { + error => { if (error != null) { return callback(error) } @@ -111,7 +113,7 @@ module.exports = UrlCache = { project_id, url, lastModified, - (error) => { + error => { if (error != null) { return callback(error) } @@ -138,23 +140,24 @@ module.exports = UrlCache = { if (lastModified == null) { return callback(null, true) } - return UrlCache._findUrlDetails(project_id, url, function ( - error, - urlDetails - ) { - if (error != null) { - return callback(error) - } - if ( - urlDetails == null || - urlDetails.lastModified == null || - urlDetails.lastModified.getTime() < lastModified.getTime() - ) { - return callback(null, true) - } else { - return callback(null, false) + return UrlCache._findUrlDetails( + project_id, + url, + function (error, urlDetails) { + if (error != null) { + return callback(error) + } + if ( + urlDetails == null || + urlDetails.lastModified == null || + urlDetails.lastModified.getTime() < lastModified.getTime() + ) { + return callback(null, true) + } else { + return callback(null, false) + } } - }) + ) }, _cacheFileNameForUrl(project_id, url) { @@ -176,14 +179,16 @@ module.exports = UrlCache = { if (error != null) { return callback(error) } - return UrlCache._deleteUrlCacheFromDisk(project_id, url, function ( - error - ) { - if (error != null) { - return callback(error) + return UrlCache._deleteUrlCacheFromDisk( + project_id, + url, + function (error) { + if (error != null) { + return callback(error) + } + return callback(null) } - return callback(null) - }) + ) }) }, @@ -191,16 +196,17 @@ module.exports = UrlCache = { if (callback == null) { callback = function (error) {} } - return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function ( - error - ) { - if (error != null && error.code !== 'ENOENT') { - // no error if the file isn't present - return callback(error) - } else { - return callback() + return fs.unlink( + UrlCache._cacheFilePathForUrl(project_id, url), + function (error) { + if (error != null && error.code !== 'ENOENT') { + // no error if the file isn't present + return callback(error) + } else { + return callback() + } } - }) + ) }, _findUrlDetails(project_id, url, callback) { @@ -208,9 +214,9 @@ module.exports = UrlCache = { callback = function (error, urlDetails) {} } const timer = new Metrics.Timer('db-find-url-details') - const job = (cb) => + const job = cb => db.UrlCache.findOne({ where: { url, project_id } }) - .then((urlDetails) => cb(null, urlDetails)) + .then(urlDetails => cb(null, urlDetails)) .error(cb) dbQueue.queue.push(job, (error, urlDetails) => { timer.done() @@ -223,7 +229,7 @@ module.exports = UrlCache = { callback = function (error) {} } const timer = new Metrics.Timer('db-update-or-create-url-details') - const job = (cb) => + const job = cb => db.UrlCache.findOrCreate({ where: { url, project_id } }) .spread((urlDetails, created) => urlDetails @@ -232,7 +238,7 @@ module.exports = UrlCache = { .error(cb) ) .error(cb) - dbQueue.queue.push(job, (error) => { + dbQueue.queue.push(job, error => { timer.done() callback(error) }) @@ -243,11 +249,11 @@ module.exports = UrlCache = { callback = function (error) {} } const timer = new Metrics.Timer('db-clear-url-details') - const job = (cb) => + const job = cb => db.UrlCache.destroy({ where: { url, project_id } }) .then(() => cb(null)) .error(cb) - dbQueue.queue.push(job, (error) => { + dbQueue.queue.push(job, error => { timer.done() callback(error) }) @@ -258,12 +264,12 @@ module.exports = UrlCache = { callback = function (error, urls) {} } const timer = new Metrics.Timer('db-find-urls-in-project') - const job = (cb) => + const job = cb => db.UrlCache.findAll({ where: { project_id } }) - .then((urlEntries) => + .then(urlEntries => cb( null, - urlEntries.map((entry) => entry.url) + urlEntries.map(entry => entry.url) ) ) .error(cb) @@ -271,5 +277,5 @@ module.exports = UrlCache = { timer.done() callback(err, urls) }) - } + }, } diff --git a/app/js/UrlFetcher.js b/app/js/UrlFetcher.js index 28155b94..4d3c3d5e 100644 --- a/app/js/UrlFetcher.js +++ b/app/js/UrlFetcher.js @@ -127,5 +127,5 @@ module.exports = UrlFetcher = { ) } }) - } + }, } diff --git a/app/js/db.js b/app/js/db.js index 135f3a52..856f1da0 100644 --- a/app/js/db.js +++ b/app/js/db.js @@ -37,10 +37,10 @@ module.exports = { { url: Sequelize.STRING, project_id: Sequelize.STRING, - lastModified: Sequelize.DATE + lastModified: Sequelize.DATE, }, { - indexes: [{ fields: ['url', 'project_id'] }, { fields: ['project_id'] }] + indexes: [{ fields: ['url', 'project_id'] }, { fields: ['project_id'] }], } ), @@ -48,10 +48,10 @@ module.exports = { 'Project', { project_id: { type: Sequelize.STRING, primaryKey: true }, - lastAccessed: Sequelize.DATE + lastAccessed: Sequelize.DATE, }, { - indexes: [{ fields: ['lastAccessed'] }] + indexes: [{ fields: ['lastAccessed'] }], } ), @@ -62,6 +62,6 @@ module.exports = { return sequelize .sync() .then(() => logger.log('db sync complete')) - .catch((err) => console.log(err, 'error syncing')) - } + .catch(err => console.log(err, 'error syncing')) + }, } diff --git a/app/lib/pdfjs/FSPdfManager.js b/app/lib/pdfjs/FSPdfManager.js index 8fb1606b..692576b2 100644 --- a/app/lib/pdfjs/FSPdfManager.js +++ b/app/lib/pdfjs/FSPdfManager.js @@ -39,5 +39,5 @@ class FSPdfManager extends LocalPdfManager { } module.exports = { - FSPdfManager + FSPdfManager, } diff --git a/app/lib/pdfjs/FSStream.js b/app/lib/pdfjs/FSStream.js index 748d7436..e3e3ac02 100644 --- a/app/lib/pdfjs/FSStream.js +++ b/app/lib/pdfjs/FSStream.js @@ -34,14 +34,14 @@ class FSStream extends Stream { const result = { begin: begin, end: end, - buffer: Buffer.alloc(end - begin, 0) + buffer: Buffer.alloc(end - begin, 0), } this.cachedBytes.push(result) return this.fh.read(result.buffer, 0, end - begin, begin) } _ensureGetPos(pos) { - const found = this.cachedBytes.find((x) => { + const found = this.cachedBytes.find(x => { return x.begin <= pos && pos < x.end }) if (!found) { @@ -52,7 +52,7 @@ class FSStream extends Stream { _ensureGetRange(begin, end) { end = Math.min(end, this.length) // BG: handle overflow case - const found = this.cachedBytes.find((x) => { + const found = this.cachedBytes.find(x => { return x.begin <= begin && end <= x.end }) if (!found) { diff --git a/app/lib/pdfjs/parseXrefTable.js b/app/lib/pdfjs/parseXrefTable.js index de7a386f..1ecfd239 100644 --- a/app/lib/pdfjs/parseXrefTable.js +++ b/app/lib/pdfjs/parseXrefTable.js @@ -23,5 +23,5 @@ async function parseXrefTable(path, size, checkDeadline) { } module.exports = { - parseXrefTable + parseXrefTable, } diff --git a/config/settings.defaults.js b/config/settings.defaults.js index b53f871b..352c045d 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -12,12 +12,12 @@ module.exports = { process.env.SQLITE_PATH || Path.resolve(__dirname, '../db/db.sqlite'), pool: { max: 1, - min: 1 + min: 1, }, retry: { - max: 10 - } - } + max: 10, + }, + }, }, compileSizeLimit: process.env.COMPILE_SIZE_LIMIT || '7mb', @@ -33,30 +33,30 @@ module.exports = { clsiCacheDir: Path.resolve(__dirname, '../cache'), synctexBaseDir(projectId) { return Path.join(this.compilesDir, projectId) - } + }, }, internal: { clsi: { port: 3013, - host: process.env.LISTEN_ADDRESS || 'localhost' + host: process.env.LISTEN_ADDRESS || 'localhost', }, load_balancer_agent: { report_load: true, load_port: 3048, - local_port: 3049 - } + local_port: 3049, + }, }, apis: { clsi: { - url: `http://${process.env.CLSI_HOST || 'localhost'}:3013` + url: `http://${process.env.CLSI_HOST || 'localhost'}:3013`, }, clsiPerf: { host: `${process.env.CLSI_PERF_HOST || 'localhost'}:${ process.env.CLSI_PERF_PORT || '3043' - }` - } + }`, + }, }, smokeTest: process.env.SMOKE_TEST || false, @@ -67,7 +67,7 @@ module.exports = { texliveImageNameOveride: process.env.TEX_LIVE_IMAGE_NAME_OVERRIDE, texliveOpenoutAny: process.env.TEXLIVE_OPENOUT_ANY, sentry: { - dsn: process.env.SENTRY_DSN + dsn: process.env.SENTRY_DSN, }, enablePdfCaching: process.env.ENABLE_PDF_CACHING === 'true', @@ -75,14 +75,13 @@ module.exports = { pdfCachingMinChunkSize: parseInt(process.env.PDF_CACHING_MIN_CHUNK_SIZE, 10) || 1024, pdfCachingMaxProcessingTime: - parseInt(process.env.PDF_CACHING_MAX_PROCESSING_TIME, 10) || 10 * 1000 + parseInt(process.env.PDF_CACHING_MAX_PROCESSING_TIME, 10) || 10 * 1000, } if (process.env.ALLOWED_COMPILE_GROUPS) { try { - module.exports.allowedCompileGroups = process.env.ALLOWED_COMPILE_GROUPS.split( - ' ' - ) + module.exports.allowedCompileGroups = + process.env.ALLOWED_COMPILE_GROUPS.split(' ') } catch (error) { console.error(error, 'could not apply allowed compile group setting') process.exit(1) @@ -98,14 +97,14 @@ if (process.env.DOCKER_RUNNER) { image: process.env.TEXLIVE_IMAGE || 'quay.io/sharelatex/texlive-full:2017.1', env: { - HOME: '/tmp' + HOME: '/tmp', }, socketPath: '/var/run/docker.sock', - user: process.env.TEXLIVE_IMAGE_USER || 'tex' + user: process.env.TEXLIVE_IMAGE_USER || 'tex', }, optimiseInDocker: true, expireProjectAfterIdleMs: 24 * 60 * 60 * 1000, - checkProjectsIntervalMs: 10 * 60 * 1000 + checkProjectsIntervalMs: 10 * 60 * 1000, } try { @@ -120,7 +119,7 @@ if (process.env.DOCKER_RUNNER) { // Automatically clean up wordcount and synctex containers const defaultCompileGroupConfig = { wordcount: { 'HostConfig.AutoRemove': true }, - synctex: { 'HostConfig.AutoRemove': true } + synctex: { 'HostConfig.AutoRemove': true }, } module.exports.clsi.docker.compileGroupConfig = Object.assign( defaultCompileGroupConfig, @@ -146,9 +145,8 @@ if (process.env.DOCKER_RUNNER) { if (process.env.ALLOWED_IMAGES) { try { - module.exports.clsi.docker.allowedImages = process.env.ALLOWED_IMAGES.split( - ' ' - ) + module.exports.clsi.docker.allowedImages = + process.env.ALLOWED_IMAGES.split(' ') } catch (error) { console.error(error, 'could not apply allowed images setting') process.exit(1) diff --git a/test/acceptance/js/AllowedImageNamesTests.js b/test/acceptance/js/AllowedImageNamesTests.js index 06304a80..e33ccb46 100644 --- a/test/acceptance/js/AllowedImageNamesTests.js +++ b/test/acceptance/js/AllowedImageNamesTests.js @@ -7,7 +7,7 @@ describe('AllowedImageNames', function () { this.project_id = Client.randomId() this.request = { options: { - imageName: undefined + imageName: undefined, }, resources: [ { @@ -17,9 +17,9 @@ describe('AllowedImageNames', function () { \\begin{document} Hello world \\end{document}\ -` - } - ] +`, + }, + ], } ClsiApp.ensureRunning(done) }) @@ -101,8 +101,8 @@ Hello world expect(error).to.not.exist expect(result).to.deep.equal({ pdf: [ - { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } - ] + { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 }, + ], }) done() } @@ -139,7 +139,7 @@ Hello world (error, result) => { expect(error).to.not.exist expect(result).to.deep.equal({ - code: [{ file: 'main.tex', line: 3, column: -1 }] + code: [{ file: 'main.tex', line: 3, column: -1 }], }) done() } diff --git a/test/acceptance/js/BrokenLatexFileTests.js b/test/acceptance/js/BrokenLatexFileTests.js index 34d3b37c..71e9956c 100644 --- a/test/acceptance/js/BrokenLatexFileTests.js +++ b/test/acceptance/js/BrokenLatexFileTests.js @@ -23,9 +23,9 @@ describe('Broken LaTeX file', function () { \\begin{documen % :( Broken \\end{documen % :(\ -` - } - ] +`, + }, + ], } this.correct_request = { resources: [ @@ -36,9 +36,9 @@ Broken \\begin{document} Hello world \\end{document}\ -` - } - ] +`, + }, + ], } return ClsiApp.ensureRunning(done) }) diff --git a/test/acceptance/js/DeleteOldFilesTest.js b/test/acceptance/js/DeleteOldFilesTest.js index 064f6be6..09eea1a9 100644 --- a/test/acceptance/js/DeleteOldFilesTest.js +++ b/test/acceptance/js/DeleteOldFilesTest.js @@ -23,9 +23,9 @@ describe('Deleting Old Files', function () { \\begin{document} Hello world \\end{document}\ -` - } - ] +`, + }, + ], } return ClsiApp.ensureRunning(done) }) diff --git a/test/acceptance/js/ExampleDocumentTests.js b/test/acceptance/js/ExampleDocumentTests.js index 92bab4b6..84758c4d 100644 --- a/test/acceptance/js/ExampleDocumentTests.js +++ b/test/acceptance/js/ExampleDocumentTests.js @@ -23,7 +23,7 @@ const ChildProcess = require('child_process') const ClsiApp = require('./helpers/ClsiApp') const logger = require('logger-sharelatex') const Path = require('path') -const fixturePath = (path) => { +const fixturePath = path => { if (path.slice(0, 3) === 'tmp') { return '/tmp/clsi_acceptance_tests' + path.slice(3) } @@ -49,8 +49,8 @@ const convertToPng = function (pdfPath, pngPath, callback) { console.log(command) const convert = ChildProcess.exec(command) const stdout = '' - convert.stdout.on('data', (chunk) => console.log('STDOUT', chunk.toString())) - convert.stderr.on('data', (chunk) => console.log('STDERR', chunk.toString())) + convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString())) + convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) return convert.on('exit', () => callback()) } @@ -65,11 +65,11 @@ const compare = function (originalPath, generatedPath, callback) { )} ${diff_file}` ) let stderr = '' - proc.stderr.on('data', (chunk) => (stderr += chunk)) + proc.stderr.on('data', chunk => (stderr += chunk)) return proc.on('exit', () => { if (stderr.trim() === '0 (0)') { // remove output diff if test matches expected image - fs.unlink(diff_file, (err) => { + fs.unlink(diff_file, err => { if (err) { throw err } @@ -88,8 +88,8 @@ const checkPdfInfo = function (pdfPath, callback) { } const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`) let stdout = '' - proc.stdout.on('data', (chunk) => (stdout += chunk)) - proc.stderr.on('data', (chunk) => console.log('STDERR', chunk.toString())) + proc.stdout.on('data', chunk => (stdout += chunk)) + proc.stderr.on('data', chunk => console.log('STDERR', chunk.toString())) return proc.on('exit', () => { if (stdout.match(/Optimized:\s+yes/)) { return callback(null, true) @@ -135,14 +135,14 @@ const comparePdf = function (project_id, example_dir, callback) { return convertToPng( `tmp/${project_id}.pdf`, `tmp/${project_id}-generated.png`, - (error) => { + error => { if (error != null) { throw error } return convertToPng( `examples/${example_dir}/output.pdf`, `tmp/${project_id}-source.png`, - (error) => { + error => { if (error != null) { throw error } @@ -162,7 +162,7 @@ const comparePdf = function (project_id, example_dir, callback) { } ) } else { - return compareMultiplePages(project_id, (error) => { + return compareMultiplePages(project_id, error => { if (error != null) { throw error } @@ -216,82 +216,71 @@ describe('Example Documents', function () { fsExtra.remove(fixturePath('tmp'), done) }) - return Array.from(fs.readdirSync(fixturePath('examples'))).map( - (example_dir) => - ((example_dir) => - describe(example_dir, function () { - before(function () { - return (this.project_id = Client.randomId() + '_' + example_dir) - }) + return Array.from(fs.readdirSync(fixturePath('examples'))).map(example_dir => + (example_dir => + describe(example_dir, function () { + before(function () { + return (this.project_id = Client.randomId() + '_' + example_dir) + }) - it('should generate the correct pdf', function (done) { - this.timeout(MOCHA_LATEX_TIMEOUT) - return Client.compileDirectory( - this.project_id, - fixturePath('examples'), - example_dir, - 4242, - (error, res, body) => { - if ( - error || - __guard__( - body != null ? body.compile : undefined, - (x) => x.status - ) === 'failure' - ) { - console.log( - 'DEBUG: error', - error, - 'body', - JSON.stringify(body) - ) - return done(new Error('Compile failed')) - } - const pdf = Client.getOutputFile(body, 'pdf') - return downloadAndComparePdf( - this.project_id, - example_dir, - pdf.url, - done - ) + it('should generate the correct pdf', function (done) { + this.timeout(MOCHA_LATEX_TIMEOUT) + return Client.compileDirectory( + this.project_id, + fixturePath('examples'), + example_dir, + 4242, + (error, res, body) => { + if ( + error || + __guard__( + body != null ? body.compile : undefined, + x => x.status + ) === 'failure' + ) { + console.log('DEBUG: error', error, 'body', JSON.stringify(body)) + return done(new Error('Compile failed')) } - ) - }) + const pdf = Client.getOutputFile(body, 'pdf') + return downloadAndComparePdf( + this.project_id, + example_dir, + pdf.url, + done + ) + } + ) + }) - return it('should generate the correct pdf on the second run as well', function (done) { - this.timeout(MOCHA_LATEX_TIMEOUT) - return Client.compileDirectory( - this.project_id, - fixturePath('examples'), - example_dir, - 4242, - (error, res, body) => { - if ( - error || - __guard__( - body != null ? body.compile : undefined, - (x) => x.status - ) === 'failure' - ) { - console.log( - 'DEBUG: error', - error, - 'body', - JSON.stringify(body) - ) - return done(new Error('Compile failed')) - } - const pdf = Client.getOutputFile(body, 'pdf') - return downloadAndComparePdf( - this.project_id, - example_dir, - pdf.url, - done - ) + return it('should generate the correct pdf on the second run as well', function (done) { + this.timeout(MOCHA_LATEX_TIMEOUT) + return Client.compileDirectory( + this.project_id, + fixturePath('examples'), + example_dir, + 4242, + (error, res, body) => { + if ( + error || + __guard__( + body != null ? body.compile : undefined, + x => x.status + ) === 'failure' + ) { + console.log('DEBUG: error', error, 'body', JSON.stringify(body)) + return done(new Error('Compile failed')) } - ) - }) - }))(example_dir) + const pdf = Client.getOutputFile(body, 'pdf') + return downloadAndComparePdf( + this.project_id, + example_dir, + pdf.url, + done + ) + } + ) + }) + }))(example_dir) ) }) diff --git a/test/acceptance/js/SimpleLatexFileTests.js b/test/acceptance/js/SimpleLatexFileTests.js index 7377afda..b2152f39 100644 --- a/test/acceptance/js/SimpleLatexFileTests.js +++ b/test/acceptance/js/SimpleLatexFileTests.js @@ -24,9 +24,9 @@ describe('Simple LaTeX file', function () { \\begin{document} Hello world \\end{document}\ -` - } - ] +`, + }, + ], } return ClsiApp.ensureRunning(() => { return Client.compile( diff --git a/test/acceptance/js/Stats.js b/test/acceptance/js/Stats.js index d96a8fcb..4f071abe 100644 --- a/test/acceptance/js/Stats.js +++ b/test/acceptance/js/Stats.js @@ -3,7 +3,7 @@ const Settings = require('@overleaf/settings') after(function (done) { request( { - url: `${Settings.apis.clsi.url}/metrics` + url: `${Settings.apis.clsi.url}/metrics`, }, (err, response, body) => { if (err) return done(err) diff --git a/test/acceptance/js/SynctexTests.js b/test/acceptance/js/SynctexTests.js index 206ada45..3bed29aa 100644 --- a/test/acceptance/js/SynctexTests.js +++ b/test/acceptance/js/SynctexTests.js @@ -27,9 +27,9 @@ Hello world resources: [ { path: 'main.tex', - content - } - ] + content, + }, + ], } this.project_id = Client.randomId() return ClsiApp.ensureRunning(() => { @@ -59,8 +59,8 @@ Hello world } expect(pdfPositions).to.deep.equal({ pdf: [ - { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 } - ] + { page: 1, h: 133.77, v: 134.76, height: 6.92, width: 343.71 }, + ], }) return done() } @@ -80,7 +80,7 @@ Hello world throw error } expect(codePositions).to.deep.equal({ - code: [{ file: 'main.tex', line: 3, column: -1 }] + code: [{ file: 'main.tex', line: 3, column: -1 }], }) return done() } @@ -132,9 +132,9 @@ Hello world resources: [ { path: 'main.tex', - content - } - ] + content, + }, + ], } Client.compile( this.broken_project_id, diff --git a/test/acceptance/js/TimeoutTests.js b/test/acceptance/js/TimeoutTests.js index 0d359bec..bca8ae71 100644 --- a/test/acceptance/js/TimeoutTests.js +++ b/test/acceptance/js/TimeoutTests.js @@ -16,7 +16,7 @@ describe('Timed out compile', function () { before(function (done) { this.request = { options: { - timeout: 10 + timeout: 10, }, // seconds resources: [ { @@ -27,9 +27,9 @@ describe('Timed out compile', function () { \\def\\x{Hello!\\par\\x} \\x \\end{document}\ -` - } - ] +`, + }, + ], } this.project_id = Client.randomId() return ClsiApp.ensureRunning(() => { @@ -55,7 +55,7 @@ describe('Timed out compile', function () { }) return it('should return the log output file name', function () { - const outputFilePaths = this.body.compile.outputFiles.map((x) => x.path) + const outputFilePaths = this.body.compile.outputFiles.map(x => x.path) return outputFilePaths.should.include('output.log') }) }) diff --git a/test/acceptance/js/UrlCachingTests.js b/test/acceptance/js/UrlCachingTests.js index 9528efba..05a8b26e 100644 --- a/test/acceptance/js/UrlCachingTests.js +++ b/test/acceptance/js/UrlCachingTests.js @@ -35,7 +35,7 @@ const Server = { randomId() { return Math.random().toString(16).slice(2) - } + }, } Server.run() @@ -55,13 +55,13 @@ describe('Url Caching', function () { \\begin{document} \\includegraphics{lion.png} \\end{document}\ -` +`, }, { path: 'lion.png', - url: `http://${host}:31415/${this.file}` - } - ] + url: `http://${host}:31415/${this.file}`, + }, + ], } sinon.spy(Server, 'getFile') @@ -102,14 +102,14 @@ describe('Url Caching', function () { \\begin{document} \\includegraphics{lion.png} \\end{document}\ -` +`, }, (this.image_resource = { path: 'lion.png', url: `http://${host}:31415/${this.file}`, - modified: Date.now() - }) - ] + modified: Date.now(), + }), + ], } return Client.compile( @@ -157,14 +157,14 @@ describe('Url Caching', function () { \\begin{document} \\includegraphics{lion.png} \\end{document}\ -` +`, }, (this.image_resource = { path: 'lion.png', url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - }) - ] + modified: (this.last_modified = Date.now()), + }), + ], } return Client.compile( @@ -213,14 +213,14 @@ describe('Url Caching', function () { \\begin{document} \\includegraphics{lion.png} \\end{document}\ -` +`, }, (this.image_resource = { path: 'lion.png', url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - }) - ] + modified: (this.last_modified = Date.now()), + }), + ], } return Client.compile( @@ -269,14 +269,14 @@ describe('Url Caching', function () { \\begin{document} \\includegraphics{lion.png} \\end{document}\ -` +`, }, (this.image_resource = { path: 'lion.png', url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - }) - ] + modified: (this.last_modified = Date.now()), + }), + ], } return Client.compile( @@ -325,17 +325,17 @@ describe('Url Caching', function () { \\begin{document} \\includegraphics{lion.png} \\end{document}\ -` +`, }, (this.image_resource = { path: 'lion.png', url: `http://${host}:31415/${this.file}`, - modified: (this.last_modified = Date.now()) - }) - ] + modified: (this.last_modified = Date.now()), + }), + ], } - return Client.compile(this.project_id, this.request, (error) => { + return Client.compile(this.project_id, this.request, error => { if (error != null) { throw error } diff --git a/test/acceptance/js/WordcountTests.js b/test/acceptance/js/WordcountTests.js index 837eb47a..d3fa7d2b 100644 --- a/test/acceptance/js/WordcountTests.js +++ b/test/acceptance/js/WordcountTests.js @@ -25,9 +25,9 @@ describe('Syncing', function () { content: fs.readFileSync( path.join(__dirname, '../fixtures/naugty_strings.txt'), 'utf-8' - ) - } - ] + ), + }, + ], } this.project_id = Client.randomId() return ClsiApp.ensureRunning(() => { @@ -61,8 +61,8 @@ describe('Syncing', function () { mathInline: 6, mathDisplay: 0, errors: 0, - messages: '' - } + messages: '', + }, }) return done() }) diff --git a/test/acceptance/js/helpers/Client.js b/test/acceptance/js/helpers/Client.js index 1da6601e..af8e0e30 100644 --- a/test/acceptance/js/helpers/Client.js +++ b/test/acceptance/js/helpers/Client.js @@ -38,8 +38,8 @@ module.exports = Client = { { url: `${this.host}/project/${project_id}/compile`, json: { - compile: data - } + compile: data, + }, }, callback ) @@ -66,7 +66,7 @@ module.exports = Client = { const app = express() app.use(express.static(directory)) console.log('starting test server on', port, host) - return app.listen(port, host).on('error', (error) => { + return app.listen(port, host).on('error', error => { console.error('error starting server:', error.message) return process.exit(1) }) @@ -87,9 +87,9 @@ module.exports = Client = { imageName, file, line, - column + column, }, - json: true + json: true, }, (error, response, body) => { if (error != null) { @@ -118,9 +118,9 @@ module.exports = Client = { imageName, page, h, - v + v, }, - json: true + json: true, }, (error, response, body) => { if (error != null) { @@ -148,7 +148,7 @@ module.exports = Client = { entities = entities.concat( fs .readdirSync(`${baseDirectory}/${directory}/${entity}`) - .map((subEntity) => { + .map(subEntity => { if (subEntity === 'main.tex') { rootResourcePath = `${entity}/${subEntity}` } @@ -167,14 +167,14 @@ module.exports = Client = { 'Rtex', 'ist', 'md', - 'Rmd' + 'Rmd', ].indexOf(extension) > -1 ) { resources.push({ path: entity, content: fs .readFileSync(`${baseDirectory}/${directory}/${entity}`) - .toString() + .toString(), }) } else if ( ['eps', 'ttf', 'png', 'jpg', 'pdf', 'jpeg'].indexOf(extension) > -1 @@ -182,7 +182,7 @@ module.exports = Client = { resources.push({ path: entity, url: `http://${host}:${serverPort}/${directory}/${entity}`, - modified: stat.mtime + modified: stat.mtime, }) } } @@ -193,7 +193,7 @@ module.exports = Client = { (error, body) => { const req = { resources, - rootResourcePath + rootResourcePath, } if (error == null) { @@ -220,8 +220,8 @@ module.exports = Client = { url: `${this.host}/project/${project_id}/wordcount`, qs: { image, - file - } + file, + }, }, (error, response, body) => { if (error != null) { @@ -233,5 +233,5 @@ module.exports = Client = { return callback(null, JSON.parse(body)) } ) - } + }, } diff --git a/test/acceptance/js/helpers/ClsiApp.js b/test/acceptance/js/helpers/ClsiApp.js index 343c3c7d..8dc946ff 100644 --- a/test/acceptance/js/helpers/ClsiApp.js +++ b/test/acceptance/js/helpers/ClsiApp.js @@ -35,10 +35,10 @@ module.exports = { return app.listen( __guard__( Settings.internal != null ? Settings.internal.clsi : undefined, - (x) => x.port + x => x.port ), 'localhost', - (error) => { + error => { if (error != null) { throw error } @@ -55,7 +55,7 @@ module.exports = { } ) } - } + }, } function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null diff --git a/test/acceptance/scripts/settings.test.js b/test/acceptance/scripts/settings.test.js index f9446c31..877ad346 100644 --- a/test/acceptance/scripts/settings.test.js +++ b/test/acceptance/scripts/settings.test.js @@ -9,8 +9,8 @@ module.exports = { username: 'clsi', password: null, dialect: 'sqlite', - storage: Path.resolve('db.sqlite') - } + storage: Path.resolve('db.sqlite'), + }, }, path: { @@ -22,7 +22,7 @@ module.exports = { synctexBaseDir() { return '/compile' }, - sandboxedCompilesHostDir: process.env.SANDBOXED_COMPILES_HOST_DIR + sandboxedCompilesHostDir: process.env.SANDBOXED_COMPILES_HOST_DIR, }, clsi: { @@ -33,32 +33,31 @@ module.exports = { docker: { image: process.env.TEXLIVE_IMAGE || 'texlive-full:2017.1-opt', env: { - PATH: - '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/', - HOME: '/tmp' + PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/2017/bin/x86_64-linux/', + HOME: '/tmp', }, modem: { - socketPath: false + socketPath: false, }, - user: process.env.SIBLING_CONTAINER_USER || '111' - } + user: process.env.SIBLING_CONTAINER_USER || '111', + }, }, internal: { clsi: { port: 3013, load_port: 3044, - host: 'localhost' - } + host: 'localhost', + }, }, apis: { clsi: { - url: 'http://localhost:3013' - } + url: 'http://localhost:3013', + }, }, smokeTest: false, project_cache_length_ms: 1000 * 60 * 60 * 24, - parallelFileDownloads: 1 + parallelFileDownloads: 1, } diff --git a/test/bench/hashbench.js b/test/bench/hashbench.js index bbd9a028..787a4e22 100644 --- a/test/bench/hashbench.js +++ b/test/bench/hashbench.js @@ -22,13 +22,13 @@ function test(hashType, filePath, callback) { return callback(err) } const t0 = process.hrtime.bigint() - ContentCacheManager.update(dir, filePath, (x) => { + ContentCacheManager.update(dir, filePath, x => { const t1 = process.hrtime.bigint() const cold = Number(t1 - t0) / 1e6 - ContentCacheManager.update(dir, filePath, (x) => { + ContentCacheManager.update(dir, filePath, x => { const t2 = process.hrtime.bigint() const warm = Number(t2 - t1) / 1e6 - fs.rmdir(dir, { recursive: true }, (err) => { + fs.rmdir(dir, { recursive: true }, err => { if (err) { return callback(err) } @@ -52,18 +52,18 @@ function test(hashType, filePath, callback) { }) } -var jobs = [] -files.forEach((file) => { - jobs.push((cb) => { +const jobs = [] +files.forEach(file => { + jobs.push(cb => { test('md5', file, cb) }) - jobs.push((cb) => { + jobs.push(cb => { test('sha1', file, cb) }) - jobs.push((cb) => { + jobs.push(cb => { test('hmac-sha1', file, cb) }) - jobs.push((cb) => { + jobs.push(cb => { test('sha256', file, cb) }) }) diff --git a/test/load/js/loadTest.js b/test/load/js/loadTest.js index 7e2ae5fa..96f15a4c 100644 --- a/test/load/js/loadTest.js +++ b/test/load/js/loadTest.js @@ -17,7 +17,7 @@ const _ = require('lodash') const concurentCompiles = 5 const totalCompiles = 50 -const buildUrl = (path) => +const buildUrl = path => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}` const mainTexContent = fs.readFileSync('./bulk.tex', 'utf-8') @@ -51,11 +51,11 @@ const makeRequest = function (compileNumber, callback) { \\begin{document} ${bodyContent} \\end{document}\ -` - } - ] - } - } +`, + }, + ], + }, + }, }, (err, response, body) => { if (response.statusCode !== 200) { @@ -74,12 +74,13 @@ ${bodyContent} ) } -const jobs = _.map(__range__(1, totalCompiles, true), (i) => (cb) => - makeRequest(i, cb) +const jobs = _.map( + __range__(1, totalCompiles, true), + i => cb => makeRequest(i, cb) ) const startTime = new Date() -async.parallelLimit(jobs, concurentCompiles, (err) => { +async.parallelLimit(jobs, concurentCompiles, err => { if (err != null) { console.error(err) } diff --git a/test/setup.js b/test/setup.js index 87532f0a..de87f662 100644 --- a/test/setup.js +++ b/test/setup.js @@ -12,8 +12,8 @@ SandboxedModule.configure({ info() {}, warn() {}, error() {}, - err() {} - } + err() {}, + }, }, - globals: { Buffer, console, process } + globals: { Buffer, console, process }, }) diff --git a/test/smoke/js/SmokeTests.js b/test/smoke/js/SmokeTests.js index 8dbdc0a3..0c207e66 100644 --- a/test/smoke/js/SmokeTests.js +++ b/test/smoke/js/SmokeTests.js @@ -1,20 +1,20 @@ const request = require('request') const Settings = require('@overleaf/settings') -const buildUrl = (path) => +const buildUrl = path => `http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}` const url = buildUrl(`project/smoketest-${process.pid}/compile`) module.exports = { sendNewResult(res) { - this._run((error) => this._sendResponse(res, error)) + this._run(error => this._sendResponse(res, error)) }, sendLastResult(res) { this._sendResponse(res, this._lastError) }, triggerRun(cb) { - this._run((error) => { + this._run(error => { this._lastError = error cb(error) }) @@ -74,11 +74,11 @@ module.exports = { } \\end{tikzpicture} \\end{document}\ -` - } - ] - } - } +`, + }, + ], + }, + }, }, (error, response, body) => { if (error) return done(error) @@ -98,5 +98,5 @@ module.exports = { done() } ) - } + }, } diff --git a/test/unit/js/CompileControllerTests.js b/test/unit/js/CompileControllerTests.js index e8739379..792c7adc 100644 --- a/test/unit/js/CompileControllerTests.js +++ b/test/unit/js/CompileControllerTests.js @@ -24,7 +24,7 @@ function tryImageNameValidation(method, imageNameField) { this.Settings.clsi = { docker: {} } this.Settings.clsi.docker.allowedImages = [ 'repo/image:tag1', - 'repo/image:tag2' + 'repo/image:tag2', ] this.res.send = sinon.stub() this.res.status = sinon.stub().returns({ send: this.res.send }) @@ -69,12 +69,12 @@ describe('CompileController', function () { '@overleaf/settings': (this.Settings = { apis: { clsi: { - url: 'http://clsi.example.com' - } - } + url: 'http://clsi.example.com', + }, + }, }), - './ProjectPersistenceManager': (this.ProjectPersistenceManager = {}) - } + './ProjectPersistenceManager': (this.ProjectPersistenceManager = {}), + }, }) this.Settings.externalUrl = 'http://www.example.com' this.req = {} @@ -85,28 +85,28 @@ describe('CompileController', function () { describe('compile', function () { beforeEach(function () { this.req.body = { - compile: 'mock-body' + compile: 'mock-body', } this.req.params = { project_id: (this.project_id = 'project-id-123') } this.request = { - compile: 'mock-parsed-request' + compile: 'mock-parsed-request', } this.request_with_project_id = { compile: this.request.compile, - project_id: this.project_id + project_id: this.project_id, } this.output_files = [ { path: 'output.pdf', type: 'pdf', size: 1337, - build: 1234 + build: 1234, }, { path: 'output.log', type: 'log', - build: 1234 - } + build: 1234, + }, ] this.RequestParser.parse = sinon .stub() @@ -155,13 +155,13 @@ describe('CompileController', function () { error: null, stats: this.stats, timings: this.timings, - outputFiles: this.output_files.map((file) => { + outputFiles: this.output_files.map(file => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, - ...file + ...file, } - }) - } + }), + }, }) .should.equal(true) }) @@ -173,13 +173,13 @@ describe('CompileController', function () { { path: 'fake_output.pdf', type: 'pdf', - build: 1234 + build: 1234, }, { path: 'output.log', type: 'log', - build: 1234 - } + build: 1234, + }, ] this.CompileManager.doCompileWithLock = sinon .stub() @@ -196,13 +196,13 @@ describe('CompileController', function () { error: null, stats: this.stats, timings: this.timings, - outputFiles: this.output_files.map((file) => { + outputFiles: this.output_files.map(file => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, - ...file + ...file, } - }) - } + }), + }, }) .should.equal(true) }) @@ -215,13 +215,13 @@ describe('CompileController', function () { path: 'output.pdf', type: 'pdf', size: 0, - build: 1234 + build: 1234, }, { path: 'output.log', type: 'log', - build: 1234 - } + build: 1234, + }, ] this.CompileManager.doCompileWithLock = sinon .stub() @@ -238,13 +238,13 @@ describe('CompileController', function () { error: null, stats: this.stats, timings: this.timings, - outputFiles: this.output_files.map((file) => { + outputFiles: this.output_files.map(file => { return { url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`, - ...file + ...file, } - }) - } + }), + }, }) .should.equal(true) }) @@ -268,8 +268,8 @@ describe('CompileController', function () { outputFiles: [], // JSON.stringify will omit these stats: undefined, - timings: undefined - } + timings: undefined, + }, }) .should.equal(true) }) @@ -295,8 +295,8 @@ describe('CompileController', function () { outputFiles: [], // JSON.stringify will omit these stats: undefined, - timings: undefined - } + timings: undefined, + }, }) .should.equal(true) }) @@ -320,8 +320,8 @@ describe('CompileController', function () { outputFiles: [], // JSON.stringify will omit these stats: undefined, - timings: undefined - } + timings: undefined, + }, }) .should.equal(true) }) @@ -338,7 +338,7 @@ describe('CompileController', function () { this.req.query = { file: this.file, line: this.line.toString(), - column: this.column.toString() + column: this.column.toString(), } this.res.json = sinon.stub() @@ -363,7 +363,7 @@ describe('CompileController', function () { it('should return the positions', function () { return this.res.json .calledWith({ - pdf: this.pdfPositions + pdf: this.pdfPositions, }) .should.equal(true) }) @@ -381,7 +381,7 @@ describe('CompileController', function () { this.req.query = { page: this.page.toString(), h: this.h.toString(), - v: this.v.toString() + v: this.v.toString(), } this.res.json = sinon.stub() @@ -400,7 +400,7 @@ describe('CompileController', function () { it('should return the positions', function () { return this.res.json .calledWith({ - code: this.codePositions + code: this.codePositions, }) .should.equal(true) }) @@ -415,7 +415,7 @@ describe('CompileController', function () { this.req.params = { project_id: this.project_id } this.req.query = { file: this.file, - image: (this.image = 'example.com/image') + image: (this.image = 'example.com/image'), } this.res.json = sinon.stub() @@ -435,7 +435,7 @@ describe('CompileController', function () { this.CompileController.wordcount(this.req, this.res, this.next) return this.res.json .calledWith({ - texcount: this.texcount + texcount: this.texcount, }) .should.equal(true) }) diff --git a/test/unit/js/CompileManagerTests.js b/test/unit/js/CompileManagerTests.js index 7f4046c2..dfedcf31 100644 --- a/test/unit/js/CompileManagerTests.js +++ b/test/unit/js/CompileManagerTests.js @@ -34,16 +34,16 @@ describe('CompileManager', function () { '@overleaf/settings': (this.Settings = { path: { compilesDir: '/compiles/dir', - outputDir: '/output/dir' + outputDir: '/output/dir', }, synctexBaseDir() { return '/compile' }, clsi: { docker: { - image: 'SOMEIMAGE' - } - } + image: 'SOMEIMAGE', + }, + }, }), child_process: (this.child_process = {}), @@ -52,8 +52,8 @@ describe('CompileManager', function () { './TikzManager': (this.TikzManager = {}), './LockManager': (this.LockManager = {}), fs: (this.fs = {}), - 'fs-extra': (this.fse = { ensureDir: sinon.stub().callsArg(1) }) - } + 'fs-extra': (this.fse = { ensureDir: sinon.stub().callsArg(1) }), + }, }) this.callback = sinon.stub() this.project_id = 'project-id-123' @@ -64,7 +64,7 @@ describe('CompileManager', function () { this.request = { resources: (this.resources = 'mock-resources'), project_id: this.project_id, - user_id: this.user_id + user_id: this.user_id, } this.output_files = ['foo', 'bar'] this.Settings.compileDir = 'compiles' @@ -132,24 +132,24 @@ describe('CompileManager', function () { this.output_files = [ { path: 'output.log', - type: 'log' + type: 'log', }, { path: 'output.pdf', - type: 'pdf' - } + type: 'pdf', + }, ] this.build_files = [ { path: 'output.log', type: 'log', - build: 1234 + build: 1234, }, { path: 'output.pdf', type: 'pdf', - build: 1234 - } + build: 1234, + }, ] this.request = { resources: (this.resources = 'mock-resources'), @@ -160,7 +160,7 @@ describe('CompileManager', function () { timeout: (this.timeout = 42000), imageName: (this.image = 'example.com/image'), flags: (this.flags = ['-file-line-error']), - compileGroup: (this.compileGroup = 'compile-group') + compileGroup: (this.compileGroup = 'compile-group'), } this.env = {} this.Settings.compileDir = 'compiles' @@ -201,7 +201,7 @@ describe('CompileManager', function () { image: this.image, flags: this.flags, environment: this.env, - compileGroup: this.compileGroup + compileGroup: this.compileGroup, }) .should.equal(true) }) @@ -254,9 +254,9 @@ describe('CompileManager', function () { environment: { CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16', CHKTEX_EXIT_ON_ERROR: 1, - CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000' + CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000', }, - compileGroup: this.compileGroup + compileGroup: this.compileGroup, }) .should.equal(true) }) @@ -279,7 +279,7 @@ describe('CompileManager', function () { image: this.image, flags: this.flags, environment: this.env, - compileGroup: this.compileGroup + compileGroup: this.compileGroup, }) .should.equal(true) }) @@ -293,7 +293,7 @@ describe('CompileManager', function () { this.fs.lstat = sinon.stub().callsArgWith(1, null, { isDirectory() { return true - } + }, }) this.proc = new EventEmitter() this.proc.stdout = new EventEmitter() @@ -315,7 +315,7 @@ describe('CompileManager', function () { '-f', '--', `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, - `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}` + `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}`, ]) .should.equal(true) }) @@ -331,7 +331,7 @@ describe('CompileManager', function () { this.fs.lstat = sinon.stub().callsArgWith(1, null, { isDirectory() { return true - } + }, }) this.proc = new EventEmitter() this.proc.stdout = new EventEmitter() @@ -354,7 +354,7 @@ describe('CompileManager', function () { '-f', '--', `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, - `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}` + `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}`, ]) .should.equal(true) }) @@ -380,7 +380,7 @@ describe('CompileManager', function () { this.column = 3 this.file_name = 'main.tex' this.child_process.execFile = sinon.stub() - return (this.Settings.path.synctexBaseDir = (project_id) => + return (this.Settings.path.synctexBaseDir = project_id => `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`) }) @@ -389,7 +389,7 @@ describe('CompileManager', function () { this.fs.stat = sinon.stub().callsArgWith(1, null, { isFile() { return true - } + }, }) this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n` this.CommandRunner.run = sinon @@ -419,7 +419,7 @@ describe('CompileManager', function () { synctex_path, file_path, this.line, - this.column + this.column, ], `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, this.Settings.clsi.docker.image, @@ -437,8 +437,8 @@ describe('CompileManager', function () { h: this.h, v: this.v, height: this.height, - width: this.width - } + width: this.width, + }, ]) .should.equal(true) }) @@ -470,7 +470,7 @@ describe('CompileManager', function () { synctex_path, file_path, this.line, - this.column + this.column, ], `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`, customImageName, @@ -487,7 +487,7 @@ describe('CompileManager', function () { this.fs.stat = sinon.stub().callsArgWith(1, null, { isFile() { return true - } + }, }) this.stdout = `NODE\t${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}\t${this.line}\t${this.column}\n` this.CommandRunner.run = sinon @@ -525,8 +525,8 @@ describe('CompileManager', function () { { file: this.file_name, line: this.line, - column: this.column - } + column: this.column, + }, ]) .should.equal(true) }) @@ -598,7 +598,7 @@ describe('CompileManager', function () { '-nocol', '-inc', this.file_path, - `-out=${this.file_path}.wc` + `-out=${this.file_path}.wc`, ] return this.CommandRunner.run @@ -625,7 +625,7 @@ describe('CompileManager', function () { mathInline: 0, mathDisplay: 0, errors: 0, - messages: '' + messages: '', }) .should.equal(true) }) diff --git a/test/unit/js/ContentCacheManagerTests.js b/test/unit/js/ContentCacheManagerTests.js index 6e8490ba..e59bd68d 100644 --- a/test/unit/js/ContentCacheManagerTests.js +++ b/test/unit/js/ContentCacheManagerTests.js @@ -91,14 +91,14 @@ describe('ContentCacheManager', function () { objectId: OBJECT_ID_1, start: START_1, end: END_1, - hash: h1 + hash: h1, }, { objectId: OBJECT_ID_2, start: START_2, end: END_2, - hash: h2 - } + hash: h2, + }, ]) }) @@ -110,14 +110,14 @@ describe('ContentCacheManager', function () { JSON.stringify({ hashAge: [ [h1, 0], - [h2, 0] + [h2, 0], ], hashSize: [ [h1, RANGE_1.byteLength], - [h2, RANGE_2.byteLength] - ] + [h2, RANGE_2.byteLength], + ], }) - ) + ), }) }) @@ -144,8 +144,8 @@ describe('ContentCacheManager', function () { objectId: OBJECT_ID_1, start: START_1, end: END_1, - hash: h1 - } + hash: h1, + }, ]) }) @@ -157,14 +157,14 @@ describe('ContentCacheManager', function () { JSON.stringify({ hashAge: [ [h1, 0], - [h2, 1] + [h2, 1], ], hashSize: [ [h1, RANGE_1.byteLength], - [h2, RANGE_2.byteLength] - ] + [h2, RANGE_2.byteLength], + ], }) - ) + ), }) }) @@ -189,8 +189,8 @@ describe('ContentCacheManager', function () { objectId: OBJECT_ID_1, start: START_1, end: END_1, - hash: h1 - } + hash: h1, + }, ]) }) @@ -200,9 +200,9 @@ describe('ContentCacheManager', function () { [Path.join(contentDir, '.state.v0.json')]: Buffer.from( JSON.stringify({ hashAge: [[h1, 0]], - hashSize: [[h1, RANGE_1.byteLength]] + hashSize: [[h1, RANGE_1.byteLength]], }) - ) + ), }) }) diff --git a/test/unit/js/DockerLockManagerTests.js b/test/unit/js/DockerLockManagerTests.js index f06f1afa..5708faf2 100644 --- a/test/unit/js/DockerLockManagerTests.js +++ b/test/unit/js/DockerLockManagerTests.js @@ -20,8 +20,8 @@ describe('LockManager', function () { beforeEach(function () { return (this.LockManager = SandboxedModule.require(modulePath, { requires: { - '@overleaf/settings': (this.Settings = { clsi: { docker: {} } }) - } + '@overleaf/settings': (this.Settings = { clsi: { docker: {} } }), + }, })) }) @@ -31,7 +31,7 @@ describe('LockManager', function () { this.callback = sinon.stub() return this.LockManager.runWithLock( 'lock-one', - (releaseLock) => + releaseLock => setTimeout(() => releaseLock(null, 'hello', 'world'), 100), (err, ...args) => { @@ -54,7 +54,7 @@ describe('LockManager', function () { this.callback2 = sinon.stub() this.LockManager.runWithLock( 'lock-one', - (releaseLock) => + releaseLock => setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100), (err, ...args) => { @@ -63,7 +63,7 @@ describe('LockManager', function () { ) return this.LockManager.runWithLock( 'lock-two', - (releaseLock) => + releaseLock => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200), (err, ...args) => { @@ -95,7 +95,7 @@ describe('LockManager', function () { this.callback2 = sinon.stub() this.LockManager.runWithLock( 'lock', - (releaseLock) => + releaseLock => setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100), (err, ...args) => { @@ -104,7 +104,7 @@ describe('LockManager', function () { ) return this.LockManager.runWithLock( 'lock', - (releaseLock) => + releaseLock => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200), (err, ...args) => { @@ -149,7 +149,7 @@ describe('LockManager', function () { } this.LockManager.runWithLock( 'lock', - (releaseLock) => + releaseLock => setTimeout( () => releaseLock(null, 'hello', 'world', 'one'), 1100 @@ -162,7 +162,7 @@ describe('LockManager', function () { ) return this.LockManager.runWithLock( 'lock', - (releaseLock) => + releaseLock => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100), (err, ...args) => { @@ -206,7 +206,7 @@ describe('LockManager', function () { } this.LockManager.runWithLock( 'lock', - (releaseLock) => + releaseLock => setTimeout( () => releaseLock(null, 'hello', 'world', 'one'), 1500 @@ -219,7 +219,7 @@ describe('LockManager', function () { ) return this.LockManager.runWithLock( 'lock', - (releaseLock) => + releaseLock => setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100), (err, ...args) => { diff --git a/test/unit/js/DockerRunnerTests.js b/test/unit/js/DockerRunnerTests.js index b47ed609..dee351ec 100644 --- a/test/unit/js/DockerRunnerTests.js +++ b/test/unit/js/DockerRunnerTests.js @@ -30,7 +30,7 @@ describe('DockerRunner', function () { requires: { '@overleaf/settings': (this.Settings = { clsi: { docker: {} }, - path: {} + path: {}, }), dockerode: (Docker = (function () { Docker = class Docker { @@ -49,21 +49,21 @@ describe('DockerRunner', function () { stat: sinon.stub().yields(null, { isDirectory() { return true - } - }) + }, + }), }), './Metrics': { Timer: (Timer = class Timer { done() {} - }) + }), }, './LockManager': { runWithLock(key, runner, callback) { return runner(callback) - } - } + }, + }, }, - globals: { Math } // used by lodash + globals: { Math }, // used by lodash }) this.Docker = Docker this.getContainer = Docker.prototype.getContainer @@ -172,10 +172,10 @@ describe('DockerRunner', function () { }) it('should re-write the bind directory', function () { - const volumes = this.DockerRunner._runAndWaitForContainer.lastCall - .args[1] + const volumes = + this.DockerRunner._runAndWaitForContainer.lastCall.args[1] return expect(volumes).to.deep.equal({ - '/some/host/dir/compiles/xyz': '/compile' + '/some/host/dir/compiles/xyz': '/compile', }) }) @@ -294,7 +294,7 @@ describe('DockerRunner', function () { beforeEach(function () { this.Settings.clsi.docker.allowedImages = [ 'repo/image:tag1', - 'repo/image:tag2' + 'repo/image:tag2', ] this.DockerRunner._runAndWaitForContainer = sinon .stub() @@ -368,9 +368,9 @@ describe('DockerRunner', function () { beforeEach(function () { this.Settings.clsi.docker.compileGroupConfig = { 'compile-group': { - 'HostConfig.newProperty': 'new-property' + 'HostConfig.newProperty': 'new-property', }, - 'other-group': { otherProperty: 'other-property' } + 'other-group': { otherProperty: 'other-property' }, } this.DockerRunner._runAndWaitForContainer = sinon .stub() @@ -388,14 +388,14 @@ describe('DockerRunner', function () { }) it('should set the docker options for the compile group', function () { - const options = this.DockerRunner._runAndWaitForContainer.lastCall - .args[0] + const options = + this.DockerRunner._runAndWaitForContainer.lastCall.args[0] return expect(options.HostConfig).to.deep.include({ Binds: ['/local/compile/directory:/compile:rw'], LogConfig: { Type: 'none', Config: {} }, CapDrop: 'ALL', SecurityOpt: ['no-new-privileges'], - newProperty: 'new-property' + newProperty: 'new-property', }) }) @@ -588,7 +588,7 @@ describe('DockerRunner', function () { this.fs.stat = sinon.stub().yields(null, { isDirectory() { return false - } + }, }) return this.DockerRunner.startContainer( this.options, @@ -715,23 +715,23 @@ describe('DockerRunner', function () { { Name: '/project-old-container-name', Id: 'old-container-id', - Created: nowInSeconds - oneHourInSeconds - 100 + Created: nowInSeconds - oneHourInSeconds - 100, }, { Name: '/project-new-container-name', Id: 'new-container-id', - Created: nowInSeconds - oneHourInSeconds + 100 + Created: nowInSeconds - oneHourInSeconds + 100, }, { Name: '/totally-not-a-project-container', Id: 'some-random-id', - Created: nowInSeconds - 2 * oneHourInSeconds - } + Created: nowInSeconds - 2 * oneHourInSeconds, + }, ] this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds this.listContainers.callsArgWith(1, null, this.containers) this.DockerRunner.destroyContainer = sinon.stub().callsArg(3) - return this.DockerRunner.destroyOldContainers((error) => { + return this.DockerRunner.destroyOldContainers(error => { this.callback(error) return done() }) @@ -778,7 +778,7 @@ describe('DockerRunner', function () { return this.DockerRunner._destroyContainer( this.containerId, false, - (err) => { + err => { this.Docker.prototype.getContainer.callCount.should.equal(1) this.Docker.prototype.getContainer .calledWith(this.containerId) @@ -792,7 +792,7 @@ describe('DockerRunner', function () { return this.DockerRunner._destroyContainer( this.containerId, true, - (err) => { + err => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove .calledWith({ force: true, v: true }) @@ -806,7 +806,7 @@ describe('DockerRunner', function () { return this.DockerRunner._destroyContainer( this.containerId, false, - (err) => { + err => { this.fakeContainer.remove.callCount.should.equal(1) this.fakeContainer.remove .calledWith({ force: false, v: true }) @@ -820,7 +820,7 @@ describe('DockerRunner', function () { return this.DockerRunner._destroyContainer( this.containerId, false, - (err) => { + err => { expect(err).to.equal(null) return done() } @@ -832,7 +832,7 @@ describe('DockerRunner', function () { this.fakeError = new Error('woops') this.fakeError.statusCode = 404 this.fakeContainer = { - remove: sinon.stub().callsArgWith(1, this.fakeError) + remove: sinon.stub().callsArgWith(1, this.fakeError), } return (this.Docker.prototype.getContainer = sinon .stub() @@ -843,7 +843,7 @@ describe('DockerRunner', function () { return this.DockerRunner._destroyContainer( this.containerId, false, - (err) => { + err => { expect(err).to.equal(null) return done() } @@ -856,7 +856,7 @@ describe('DockerRunner', function () { this.fakeError = new Error('woops') this.fakeError.statusCode = 500 this.fakeContainer = { - remove: sinon.stub().callsArgWith(1, this.fakeError) + remove: sinon.stub().callsArgWith(1, this.fakeError), } return (this.Docker.prototype.getContainer = sinon .stub() @@ -867,7 +867,7 @@ describe('DockerRunner', function () { return this.DockerRunner._destroyContainer( this.containerId, false, - (err) => { + err => { expect(err).to.not.equal(null) expect(err).to.equal(this.fakeError) return done() @@ -887,7 +887,7 @@ describe('DockerRunner', function () { }) it('should get the container', function (done) { - return this.DockerRunner.kill(this.containerId, (err) => { + return this.DockerRunner.kill(this.containerId, err => { this.Docker.prototype.getContainer.callCount.should.equal(1) this.Docker.prototype.getContainer .calledWith(this.containerId) @@ -897,14 +897,14 @@ describe('DockerRunner', function () { }) it('should try to force-destroy the container', function (done) { - return this.DockerRunner.kill(this.containerId, (err) => { + return this.DockerRunner.kill(this.containerId, err => { this.fakeContainer.kill.callCount.should.equal(1) return done() }) }) it('should not produce an error', function (done) { - return this.DockerRunner.kill(this.containerId, (err) => { + return this.DockerRunner.kill(this.containerId, err => { expect(err).to.equal(undefined) return done() }) @@ -917,7 +917,7 @@ describe('DockerRunner', function () { this.fakeError.message = 'Cannot kill container <whatever> is not running' this.fakeContainer = { - kill: sinon.stub().callsArgWith(0, this.fakeError) + kill: sinon.stub().callsArgWith(0, this.fakeError), } return (this.Docker.prototype.getContainer = sinon .stub() @@ -925,7 +925,7 @@ describe('DockerRunner', function () { }) return it('should not produce an error', function (done) { - return this.DockerRunner.kill(this.containerId, (err) => { + return this.DockerRunner.kill(this.containerId, err => { expect(err).to.equal(undefined) return done() }) @@ -938,7 +938,7 @@ describe('DockerRunner', function () { this.fakeError.statusCode = 500 this.fakeError.message = 'Totally legitimate reason to throw an error' this.fakeContainer = { - kill: sinon.stub().callsArgWith(0, this.fakeError) + kill: sinon.stub().callsArgWith(0, this.fakeError), } return (this.Docker.prototype.getContainer = sinon .stub() @@ -946,7 +946,7 @@ describe('DockerRunner', function () { }) return it('should produce an error', function (done) { - return this.DockerRunner.kill(this.containerId, (err) => { + return this.DockerRunner.kill(this.containerId, err => { expect(err).to.not.equal(undefined) expect(err).to.equal(this.fakeError) return done() diff --git a/test/unit/js/DraftModeManagerTests.js b/test/unit/js/DraftModeManagerTests.js index 13291e02..b2391554 100644 --- a/test/unit/js/DraftModeManagerTests.js +++ b/test/unit/js/DraftModeManagerTests.js @@ -19,8 +19,8 @@ describe('DraftModeManager', function () { beforeEach(function () { return (this.DraftModeManager = SandboxedModule.require(modulePath, { requires: { - fs: (this.fs = {}) - } + fs: (this.fs = {}), + }, })) }) diff --git a/test/unit/js/LatexRunnerTests.js b/test/unit/js/LatexRunnerTests.js index 13858ff4..16f40bd7 100644 --- a/test/unit/js/LatexRunnerTests.js +++ b/test/unit/js/LatexRunnerTests.js @@ -25,19 +25,19 @@ describe('LatexRunner', function () { requires: { '@overleaf/settings': (this.Settings = { docker: { - socketPath: '/var/run/docker.sock' - } + socketPath: '/var/run/docker.sock', + }, }), './Metrics': { Timer: (Timer = class Timer { done() {} - }) + }), }, './CommandRunner': (this.CommandRunner = {}), fs: (this.fs = { - writeFile: sinon.stub().callsArg(2) - }) - } + writeFile: sinon.stub().callsArg(2), + }), + }, }) this.directory = '/local/compile/directory' @@ -54,7 +54,7 @@ describe('LatexRunner', function () { beforeEach(function () { return (this.CommandRunner.run = sinon.stub().callsArgWith(7, null, { stdout: 'this is stdout', - stderr: 'this is stderr' + stderr: 'this is stderr', })) }) @@ -69,7 +69,7 @@ describe('LatexRunner', function () { timeout: (this.timeout = 42000), image: this.image, environment: this.env, - compileGroup: this.compileGroup + compileGroup: this.compileGroup, }, (error, output, stats, timings) => { this.timings = timings @@ -116,7 +116,7 @@ describe('LatexRunner', function () { '\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' + '\tUser time (seconds): 0.28\n' + '\tSystem time (seconds): 0.70\n' + - '\tPercent of CPU this job got: 98%\n' + '\tPercent of CPU this job got: 98%\n', }) this.LatexRunner.runLatex( this.project_id, @@ -127,7 +127,7 @@ describe('LatexRunner', function () { timeout: (this.timeout = 42000), image: this.image, environment: this.env, - compileGroup: this.compileGroup + compileGroup: this.compileGroup, }, (error, output, stats, timings) => { this.timings = timings @@ -152,7 +152,7 @@ describe('LatexRunner', function () { mainFile: 'main-file.Rtex', compiler: this.compiler, image: this.image, - timeout: (this.timeout = 42000) + timeout: (this.timeout = 42000), }, this.callback ) @@ -175,7 +175,7 @@ describe('LatexRunner', function () { compiler: this.compiler, image: this.image, timeout: (this.timeout = 42000), - flags: ['-file-line-error', '-halt-on-error'] + flags: ['-file-line-error', '-halt-on-error'], }, this.callback ) @@ -184,7 +184,7 @@ describe('LatexRunner', function () { return it('should include the flags in the command', function () { const command = this.CommandRunner.run.args[0][1] const flags = command.filter( - (arg) => arg === '-file-line-error' || arg === '-halt-on-error' + arg => arg === '-file-line-error' || arg === '-halt-on-error' ) flags.length.should.equal(2) flags[0].should.equal('-file-line-error') diff --git a/test/unit/js/LockManagerTests.js b/test/unit/js/LockManagerTests.js index 4f9cc31f..e1090548 100644 --- a/test/unit/js/LockManagerTests.js +++ b/test/unit/js/LockManagerTests.js @@ -25,10 +25,10 @@ describe('DockerLockManager', function () { '@overleaf/settings': {}, fs: { lstat: sinon.stub().callsArgWith(1), - readdir: sinon.stub().callsArgWith(1) + readdir: sinon.stub().callsArgWith(1), }, - lockfile: (this.Lockfile = {}) - } + lockfile: (this.Lockfile = {}), + }, }) return (this.lockFile = '/local/compile/directory/.project-lock') }) diff --git a/test/unit/js/OutputFileFinderTests.js b/test/unit/js/OutputFileFinderTests.js index d094f7c6..9d1eb438 100644 --- a/test/unit/js/OutputFileFinderTests.js +++ b/test/unit/js/OutputFileFinderTests.js @@ -25,11 +25,11 @@ describe('OutputFileFinder', function () { this.OutputFileFinder = SandboxedModule.require(modulePath, { requires: { fs: (this.fs = {}), - child_process: { spawn: (this.spawn = sinon.stub()) } + child_process: { spawn: (this.spawn = sinon.stub()) }, }, globals: { - Math // used by lodash - } + Math, // used by lodash + }, }) this.directory = '/test/dir' return (this.callback = sinon.stub()) @@ -57,12 +57,12 @@ describe('OutputFileFinder', function () { return expect(this.outputFiles).to.deep.equal([ { path: 'output.pdf', - type: 'pdf' + type: 'pdf', }, { path: 'extra/file.tex', - type: 'tex' - } + type: 'tex', + }, ]) }) }) diff --git a/test/unit/js/OutputFileOptimiserTests.js b/test/unit/js/OutputFileOptimiserTests.js index 4c786660..6a0a015d 100644 --- a/test/unit/js/OutputFileOptimiserTests.js +++ b/test/unit/js/OutputFileOptimiserTests.js @@ -27,9 +27,9 @@ describe('OutputFileOptimiser', function () { fs: (this.fs = {}), path: (this.Path = {}), child_process: { spawn: (this.spawn = sinon.stub()) }, - './Metrics': {} + './Metrics': {}, }, - globals: { Math } // used by lodash + globals: { Math }, // used by lodash }) this.directory = '/test/dir' return (this.callback = sinon.stub()) diff --git a/test/unit/js/ProjectPersistenceManagerTests.js b/test/unit/js/ProjectPersistenceManagerTests.js index be8050aa..eceaf898 100644 --- a/test/unit/js/ProjectPersistenceManagerTests.js +++ b/test/unit/js/ProjectPersistenceManagerTests.js @@ -32,11 +32,11 @@ describe('ProjectPersistenceManager', function () { path: { compilesDir: '/compiles', outputDir: '/output', - clsiCacheDir: '/cache' - } + clsiCacheDir: '/cache', + }, }), - './db': (this.db = {}) - } + './db': (this.db = {}), + }, }) this.callback = sinon.stub() this.project_id = 'project-id-123' @@ -47,7 +47,7 @@ describe('ProjectPersistenceManager', function () { it('should leave expiry alone if plenty of disk', function (done) { this.diskusage.check.resolves({ available: 40, - total: 100 + total: 100, }) this.ProjectPersistenceManager.refreshExpiryTimeout(() => { @@ -61,7 +61,7 @@ describe('ProjectPersistenceManager', function () { it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function (done) { this.diskusage.check.resolves({ available: 5, - total: 100 + total: 100, }) this.ProjectPersistenceManager.refreshExpiryTimeout(() => { @@ -73,7 +73,7 @@ describe('ProjectPersistenceManager', function () { it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function (done) { this.diskusage.check.resolves({ available: 5, - total: 100 + total: 100, }) this.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500 this.ProjectPersistenceManager.refreshExpiryTimeout(() => { @@ -105,7 +105,7 @@ describe('ProjectPersistenceManager', function () { }) it('should clear each expired project', function () { - return Array.from(this.project_ids).map((project_id) => + return Array.from(this.project_ids).map(project_id => this.ProjectPersistenceManager.clearProjectFromCache .calledWith(project_id) .should.equal(true) diff --git a/test/unit/js/RequestParserTests.js b/test/unit/js/RequestParserTests.js index 840e55dd..48364990 100644 --- a/test/unit/js/RequestParserTests.js +++ b/test/unit/js/RequestParserTests.js @@ -25,7 +25,7 @@ describe('RequestParser', function () { this.validResource = { path: 'main.tex', date: '12:00 01/02/03', - content: 'Hello world' + content: 'Hello world', } this.validRequest = { compile: { @@ -33,15 +33,15 @@ describe('RequestParser', function () { options: { imageName: 'basicImageName/here:2017-1', compiler: 'pdflatex', - timeout: 42 + timeout: 42, }, - resources: [] - } + resources: [], + }, } return (this.RequestParser = SandboxedModule.require(modulePath, { requires: { - '@overleaf/settings': (this.settings = {}) - } + '@overleaf/settings': (this.settings = {}), + }, })) }) @@ -118,7 +118,7 @@ describe('RequestParser', function () { this.settings.clsi = { docker: {} } this.settings.clsi.docker.allowedImages = [ 'repo/name:tag1', - 'repo/name:tag2' + 'repo/name:tag2', ] }) @@ -402,7 +402,7 @@ describe('RequestParser', function () { this.validRequest.compile.resources.push({ path: this.badPath, date: '12:00 01/02/03', - content: 'Hello world' + content: 'Hello world', }) this.RequestParser.parse(this.validRequest, this.callback) return (this.data = this.callback.args[0][1]) diff --git a/test/unit/js/ResourceStateManagerTests.js b/test/unit/js/ResourceStateManagerTests.js index 1536299c..0a97d7b7 100644 --- a/test/unit/js/ResourceStateManagerTests.js +++ b/test/unit/js/ResourceStateManagerTests.js @@ -25,14 +25,14 @@ describe('ResourceStateManager', function () { singleOnly: true, requires: { fs: (this.fs = {}), - './SafeReader': (this.SafeReader = {}) - } + './SafeReader': (this.SafeReader = {}), + }, }) this.basePath = '/path/to/write/files/to' this.resources = [ { path: 'resource-1-mock' }, { path: 'resource-2-mock' }, - { path: 'resource-3-mock' } + { path: 'resource-3-mock' }, ] this.state = '1234567890' this.resourceFileName = `${this.basePath}/.project-sync-state` @@ -175,7 +175,7 @@ describe('ResourceStateManager', function () { this.allFiles = [ this.resources[0].path, this.resources[1].path, - this.resources[2].path + this.resources[2].path, ] return this.ResourceStateManager.checkResourceFiles( this.resources, @@ -220,7 +220,7 @@ describe('ResourceStateManager', function () { this.allFiles = [ this.resources[0].path, this.resources[1].path, - this.resources[2].path + this.resources[2].path, ] return this.ResourceStateManager.checkResourceFiles( this.resources, diff --git a/test/unit/js/ResourceWriterTests.js b/test/unit/js/ResourceWriterTests.js index 267fc2c3..4d69f557 100644 --- a/test/unit/js/ResourceWriterTests.js +++ b/test/unit/js/ResourceWriterTests.js @@ -27,7 +27,7 @@ describe('ResourceWriter', function () { requires: { fs: (this.fs = { mkdir: sinon.stub().callsArg(1), - unlink: sinon.stub().callsArg(1) + unlink: sinon.stub().callsArg(1), }), './ResourceStateManager': (this.ResourceStateManager = {}), wrench: (this.wrench = {}), @@ -43,9 +43,9 @@ describe('ResourceWriter', function () { } Timer.initClass() return Timer - })()) - }) - } + })()), + }), + }, }) this.project_id = 'project-id-123' this.basePath = '/path/to/write/files/to' @@ -62,7 +62,7 @@ describe('ResourceWriter', function () { { project_id: this.project_id, syncState: (this.syncState = '0123456789abcdef'), - resources: this.resources + resources: this.resources, }, this.basePath, this.callback @@ -76,7 +76,7 @@ describe('ResourceWriter', function () { }) it('should write each resource to disk', function () { - return Array.from(this.resources).map((resource) => + return Array.from(this.resources).map(resource => this.ResourceWriter._writeResourceToDisk .calledWith(this.project_id, resource, this.basePath) .should.equal(true) @@ -111,7 +111,7 @@ describe('ResourceWriter', function () { project_id: this.project_id, syncType: 'incremental', syncState: (this.syncState = '1234567890abcdef'), - resources: this.resources + resources: this.resources, }, this.basePath, this.callback @@ -137,7 +137,7 @@ describe('ResourceWriter', function () { }) it('should write each resource to disk', function () { - return Array.from(this.resources).map((resource) => + return Array.from(this.resources).map(resource => this.ResourceWriter._writeResourceToDisk .calledWith(this.project_id, resource, this.basePath) .should.equal(true) @@ -160,7 +160,7 @@ describe('ResourceWriter', function () { project_id: this.project_id, syncType: 'incremental', syncState: (this.syncState = '1234567890abcdef'), - resources: this.resources + resources: this.resources, }, this.basePath, this.callback @@ -183,58 +183,57 @@ describe('ResourceWriter', function () { this.output_files = [ { path: 'output.pdf', - type: 'pdf' + type: 'pdf', }, { path: 'extra/file.tex', - type: 'tex' + type: 'tex', }, { path: 'extra.aux', - type: 'aux' + type: 'aux', }, { - path: 'cache/_chunk1' + path: 'cache/_chunk1', }, { path: 'figures/image-eps-converted-to.pdf', - type: 'pdf' + type: 'pdf', }, { path: 'foo/main-figure0.md5', - type: 'md5' + type: 'md5', }, { path: 'foo/main-figure0.dpth', - type: 'dpth' + type: 'dpth', }, { path: 'foo/main-figure0.pdf', - type: 'pdf' + type: 'pdf', }, { path: '_minted-main/default-pyg-prefix.pygstyle', - type: 'pygstyle' + type: 'pygstyle', }, { path: '_minted-main/default.pygstyle', - type: 'pygstyle' + type: 'pygstyle', }, { - path: - '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex', - type: 'pygtex' + path: '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex', + type: 'pygtex', }, { path: '_markdown_main/30893013dec5d869a415610079774c2f.md.tex', - type: 'tex' + type: 'tex', }, { - path: 'output.stdout' + path: 'output.stdout', }, { - path: 'output.stderr' - } + path: 'output.stderr', + }, ] this.resources = 'mock-resources' this.OutputFileFinder.findOutputFiles = sinon @@ -368,7 +367,7 @@ describe('ResourceWriter', function () { this.resource = { path: 'main.tex', url: 'http://www.example.com/main.tex', - modified: Date.now() + modified: Date.now(), } this.UrlCache.downloadUrlToFile = sinon .stub() @@ -413,7 +412,7 @@ describe('ResourceWriter', function () { beforeEach(function () { this.resource = { path: 'main.tex', - content: 'Hello world' + content: 'Hello world', } this.fs.writeFile = sinon.stub().callsArg(2) this.fs.mkdir = sinon.stub().callsArg(2) @@ -451,7 +450,7 @@ describe('ResourceWriter', function () { beforeEach(function () { this.resource = { path: '../../main.tex', - content: 'Hello world' + content: 'Hello world', } this.fs.writeFile = sinon.stub().callsArg(2) return this.ResourceWriter._writeResourceToDisk( diff --git a/test/unit/js/StaticServerForbidSymlinksTests.js b/test/unit/js/StaticServerForbidSymlinksTests.js index 86e279e6..dd67506b 100644 --- a/test/unit/js/StaticServerForbidSymlinksTests.js +++ b/test/unit/js/StaticServerForbidSymlinksTests.js @@ -23,16 +23,16 @@ describe('StaticServerForbidSymlinks', function () { beforeEach(function () { this.settings = { path: { - compilesDir: '/compiles/here' - } + compilesDir: '/compiles/here', + }, } this.fs = {} this.ForbidSymlinks = SandboxedModule.require(modulePath, { requires: { '@overleaf/settings': this.settings, - fs: this.fs - } + fs: this.fs, + }, }) this.dummyStatic = (rootDir, options) => (req, res, next) => @@ -46,8 +46,8 @@ describe('StaticServerForbidSymlinks', function () { ) this.req = { params: { - project_id: '12345' - } + project_id: '12345', + }, } this.res = {} diff --git a/test/unit/js/TikzManager.js b/test/unit/js/TikzManager.js index 30a38075..8e011949 100644 --- a/test/unit/js/TikzManager.js +++ b/test/unit/js/TikzManager.js @@ -21,8 +21,8 @@ describe('TikzManager', function () { requires: { './ResourceWriter': (this.ResourceWriter = {}), './SafeReader': (this.SafeReader = {}), - fs: (this.fs = {}) - } + fs: (this.fs = {}), + }, })) }) diff --git a/test/unit/js/UrlCacheTests.js b/test/unit/js/UrlCacheTests.js index 32e175f7..33decd34 100644 --- a/test/unit/js/UrlCacheTests.js +++ b/test/unit/js/UrlCacheTests.js @@ -25,10 +25,10 @@ describe('UrlCache', function () { './db': {}, './UrlFetcher': (this.UrlFetcher = {}), '@overleaf/settings': (this.Settings = { - path: { clsiCacheDir: '/cache/dir' } + path: { clsiCacheDir: '/cache/dir' }, }), - fs: (this.fs = { copyFile: sinon.stub().yields() }) - } + fs: (this.fs = { copyFile: sinon.stub().yields() }), + }, })) }) @@ -339,7 +339,7 @@ describe('UrlCache', function () { }) it('should clear the cache for each url in the project', function () { - return Array.from(this.urls).map((url) => + return Array.from(this.urls).map(url => this.UrlCache._clearUrlFromCache .calledWith(this.project_id, url) .should.equal(true) diff --git a/test/unit/js/UrlFetcherTests.js b/test/unit/js/UrlFetcherTests.js index ac94540f..8e79bced 100644 --- a/test/unit/js/UrlFetcherTests.js +++ b/test/unit/js/UrlFetcherTests.js @@ -21,17 +21,17 @@ describe('UrlFetcher', function () { return (this.UrlFetcher = SandboxedModule.require(modulePath, { requires: { request: { - defaults: (this.defaults = sinon.stub().returns((this.request = {}))) + defaults: (this.defaults = sinon.stub().returns((this.request = {}))), }, fs: (this.fs = {}), '@overleaf/settings': (this.settings = { apis: { clsiPerf: { - host: 'localhost:3043' - } - } - }) - } + host: 'localhost:3043', + }, + }, + }), + }, })) }) describe('pipeUrlToFileWithRetry', function () { @@ -41,7 +41,7 @@ describe('UrlFetcher', function () { it('should call pipeUrlToFile', function (done) { this.UrlFetcher.pipeUrlToFile.callsArgWith(2) - this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, (err) => { + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { expect(err).to.equal(undefined) this.UrlFetcher.pipeUrlToFile.called.should.equal(true) done() @@ -51,7 +51,7 @@ describe('UrlFetcher', function () { it('should call pipeUrlToFile multiple times on error', function (done) { const error = new Error("couldn't download file") this.UrlFetcher.pipeUrlToFile.callsArgWith(2, error) - this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, (err) => { + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { expect(err).to.equal(error) this.UrlFetcher.pipeUrlToFile.callCount.should.equal(3) done() @@ -61,7 +61,7 @@ describe('UrlFetcher', function () { it('should call pipeUrlToFile twice if only 1 error', function (done) { this.UrlFetcher.pipeUrlToFile.onCall(0).callsArgWith(2, 'error') this.UrlFetcher.pipeUrlToFile.onCall(1).callsArgWith(2) - this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, (err) => { + this.UrlFetcher.pipeUrlToFileWithRetry(this.url, this.path, err => { expect(err).to.equal(undefined) this.UrlFetcher.pipeUrlToFile.callCount.should.equal(2) done() @@ -181,7 +181,7 @@ describe('UrlFetcher', function () { describe('with non success status code', function () { beforeEach(function (done) { - this.UrlFetcher.pipeUrlToFile(this.url, this.path, (err) => { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { this.callback(err) return done() }) @@ -202,7 +202,7 @@ describe('UrlFetcher', function () { return describe('with error', function () { beforeEach(function (done) { - this.UrlFetcher.pipeUrlToFile(this.url, this.path, (err) => { + this.UrlFetcher.pipeUrlToFile(this.url, this.path, err => { this.callback(err) return done() }) diff --git a/test/unit/lib/pdfjsTests.js b/test/unit/lib/pdfjsTests.js index 72b36a33..f90c6f49 100644 --- a/test/unit/lib/pdfjsTests.js +++ b/test/unit/lib/pdfjsTests.js @@ -28,14 +28,14 @@ async function loadContext(example) { const snapshot = blob ? JSON.parse(blob) : null return { size, - snapshot + snapshot, } } async function backFillSnapshot(example, size) { const table = await parseXrefTable(pdfPath(example), size, () => {}) await fs.promises.mkdir(Path.dirname(snapshotPath(example)), { - recursive: true + recursive: true, }) await fs.promises.writeFile( snapshotPath(example), From 22650ffd0a400b04f0ed6bb9f581a044738cfd5f Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 13 Jul 2021 12:21:14 +0100 Subject: [PATCH 704/709] [misc] temporary override a few new/changed eslint rules --- .eslintrc | 15 ++++++++++++++- app/js/RequestParser.js | 1 - test/load/js/loadTest.js | 3 --- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.eslintrc b/.eslintrc index 1c14f50e..a97661b1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "extends": [ "eslint:recommended", "standard", - "prettier", + "prettier" ], "parserOptions": { "ecmaVersion": 2018 @@ -20,6 +20,19 @@ "mocha": true }, "rules": { + // TODO(das7pad): remove overrides after fixing all the violations manually (https://github.com/overleaf/issues/issues/3882#issuecomment-878999671) + // START of temporary overrides + "array-callback-return": "off", + "no-dupe-else-if": "off", + "no-var": "off", + "no-empty": "off", + "node/handle-callback-err": "off", + "no-loss-of-precision": "off", + "node/no-callback-literal": "off", + "node/no-path-concat": "off", + "prefer-regex-literals": "off", + // END of temporary overrides + // Swap the no-unused-expressions rule with a more chai-friendly one "no-unused-expressions": 0, "chai-friendly/no-unused-expressions": "error", diff --git a/app/js/RequestParser.js b/app/js/RequestParser.js index cb097f64..04502e42 100644 --- a/app/js/RequestParser.js +++ b/app/js/RequestParser.js @@ -4,7 +4,6 @@ no-throw-literal, no-unused-vars, no-useless-escape, - standard/no-callback-literal, valid-typeof, */ // TODO: This file was created by bulk-decaffeinate. diff --git a/test/load/js/loadTest.js b/test/load/js/loadTest.js index 96f15a4c..f5e1fce0 100644 --- a/test/load/js/loadTest.js +++ b/test/load/js/loadTest.js @@ -1,6 +1,3 @@ -/* eslint-disable - standard/no-callback-literal, -*/ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* From fa3ab13faa939d2327664ee8a02177c2ea5b3fb7 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 13 Jul 2021 12:26:36 +0100 Subject: [PATCH 705/709] [misc] upgrade node version to latest v12 LTS version 12.22.3 --- .nvmrc | 2 +- Dockerfile | 2 +- buildscript.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nvmrc b/.nvmrc index e68b8603..5a80a7e9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.21.0 +12.22.3 diff --git a/Dockerfile b/Dockerfile index b02e828a..8bd4b9c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Instead run bin/update_build_scripts from # https://github.com/sharelatex/sharelatex-dev-environment -FROM node:12.21.0 as base +FROM node:12.22.3 as base WORKDIR /app COPY install_deps.sh /app diff --git a/buildscript.txt b/buildscript.txt index e04c1353..6e927a88 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -4,6 +4,6 @@ clsi --docker-repos=gcr.io/overleaf-ops --env-add= --env-pass-through=TEXLIVE_IMAGE ---node-version=12.21.0 +--node-version=12.22.3 --public-repo=True --script-version=3.11.0 From d0a56a5403c4ca30820c483b94231594797f60db Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Tue, 20 Jul 2021 14:12:42 +0100 Subject: [PATCH 706/709] [perf] trim down install_deps.sh -- install docker cli only --- install_deps.sh | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/install_deps.sh b/install_deps.sh index 49bdc5c9..f0cc386f 100755 --- a/install_deps.sh +++ b/install_deps.sh @@ -1,4 +1,24 @@ -/bin/sh -wget -qO- https://get.docker.com/ | sh -apt-get install poppler-utils vim ghostscript --yes -npm rebuild +#!/bin/bash +set -ex + +apt-get update +apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + lsb-release + +curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +echo \ + "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \ + $(lsb_release -cs) stable" \ + > /etc/apt/sources.list.d/docker.list +apt-get update + +apt-get install -y \ + docker-ce-cli \ + poppler-utils \ + ghostscript \ + +rm -rf /var/lib/apt/lists/* From 8ad2220e89d861e9eb0e44b9ee3f29aa4cdbe24c Mon Sep 17 00:00:00 2001 From: Brian Gough <brian.gough@overleaf.com> Date: Wed, 21 Jul 2021 14:53:35 +0100 Subject: [PATCH 707/709] add option for apparmor profile --- app/js/DockerRunner.js | 6 ++++++ config/settings.defaults.js | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/app/js/DockerRunner.js b/app/js/DockerRunner.js index 5de05868..28d7636f 100644 --- a/app/js/DockerRunner.js +++ b/app/js/DockerRunner.js @@ -270,6 +270,12 @@ const DockerRunner = { ) } + if (Settings.clsi.docker.apparmor_profile != null) { + options.HostConfig.SecurityOpt.push( + `apparmor=${Settings.clsi.docker.apparmor_profile}` + ) + } + if (Settings.clsi.docker.runtime) { options.HostConfig.Runtime = Settings.clsi.docker.runtime } diff --git a/config/settings.defaults.js b/config/settings.defaults.js index 352c045d..e36334df 100644 --- a/config/settings.defaults.js +++ b/config/settings.defaults.js @@ -143,6 +143,15 @@ if (process.env.DOCKER_RUNNER) { process.exit(1) } + if (process.env.APPARMOR_PROFILE) { + try { + module.exports.clsi.docker.apparmor_profile = process.env.APPARMOR_PROFILE + } catch (error) { + console.error(error, 'could not apply apparmor profile setting') + process.exit(1) + } + } + if (process.env.ALLOWED_IMAGES) { try { module.exports.clsi.docker.allowedImages = From 57993144d91aac421f0760cb535e16786f70ca15 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann <jakob.ackermann@overleaf.com> Date: Mon, 26 Jul 2021 12:14:56 +0100 Subject: [PATCH 708/709] [misc] make build scripts happy again - move pdf.js tests into test/unit/js - add env override to build script config file - update build scripts --- buildscript.txt | 2 +- package.json | 2 +- test/unit/{lib => js}/pdfjsTests.js | 2 +- .../snapshots => js/snapshots/pdfjs}/asymptote/XrefTable.json | 0 .../snapshots/pdfjs}/biber_bibliography/XrefTable.json | 0 .../snapshots => js/snapshots/pdfjs}/epstopdf/XrefTable.json | 0 .../{lib/snapshots => js/snapshots/pdfjs}/feynmf/XrefTable.json | 0 .../{lib/snapshots => js/snapshots/pdfjs}/feynmp/XrefTable.json | 0 .../snapshots => js/snapshots/pdfjs}/fontawesome/XrefTable.json | 0 .../snapshots/pdfjs}/fontawesome_xelatex/XrefTable.json | 0 .../snapshots => js/snapshots/pdfjs}/glossaries/XrefTable.json | 0 .../snapshots => js/snapshots/pdfjs}/gnuplot/XrefTable.json | 0 .../{lib/snapshots => js/snapshots/pdfjs}/hebrew/XrefTable.json | 0 .../{lib/snapshots => js/snapshots/pdfjs}/knitr/XrefTable.json | 0 .../snapshots => js/snapshots/pdfjs}/knitr_utf8/XrefTable.json | 0 .../snapshots/pdfjs}/latex_compiler/XrefTable.json | 0 .../snapshots/pdfjs}/lualatex_compiler/XrefTable.json | 0 .../snapshots/pdfjs}/makeindex-custom-style/XrefTable.json | 0 .../snapshots => js/snapshots/pdfjs}/makeindex/XrefTable.json | 0 .../{lib/snapshots => js/snapshots/pdfjs}/minted/XrefTable.json | 0 .../snapshots/pdfjs}/multibib_bibliography/XrefTable.json | 0 .../snapshots/pdfjs}/nomenclature/XrefTable.json | 0 .../snapshots/pdfjs}/references_in_include/XrefTable.json | 0 .../snapshots/pdfjs}/simple_bibliography/XrefTable.json | 0 .../snapshots/pdfjs}/subdirectories/XrefTable.json | 0 .../snapshots/pdfjs}/tikz_feynman/XrefTable.json | 0 .../snapshots/pdfjs}/xelatex_compiler/XrefTable.json | 0 27 files changed, 3 insertions(+), 3 deletions(-) rename test/unit/{lib => js}/pdfjsTests.js (97%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/asymptote/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/biber_bibliography/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/epstopdf/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/feynmf/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/feynmp/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/fontawesome/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/fontawesome_xelatex/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/glossaries/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/gnuplot/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/hebrew/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/knitr/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/knitr_utf8/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/latex_compiler/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/lualatex_compiler/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/makeindex-custom-style/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/makeindex/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/minted/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/multibib_bibliography/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/nomenclature/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/references_in_include/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/simple_bibliography/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/subdirectories/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/tikz_feynman/XrefTable.json (100%) rename test/unit/{lib/snapshots => js/snapshots/pdfjs}/xelatex_compiler/XrefTable.json (100%) diff --git a/buildscript.txt b/buildscript.txt index 6e927a88..9b755dfe 100644 --- a/buildscript.txt +++ b/buildscript.txt @@ -2,7 +2,7 @@ clsi --data-dirs=cache,compiles,db,output --dependencies= --docker-repos=gcr.io/overleaf-ops ---env-add= +--env-add=ENABLE_PDF_CACHING="true" --env-pass-through=TEXLIVE_IMAGE --node-version=12.22.3 --public-repo=True diff --git a/package.json b/package.json index 4f22ebd7..0b2b011d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "node $NODE_APP_OPTIONS app.js", "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP", - "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js test/unit/lib", + "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js", "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", "lint": "eslint --max-warnings 0 --format unix .", diff --git a/test/unit/lib/pdfjsTests.js b/test/unit/js/pdfjsTests.js similarity index 97% rename from test/unit/lib/pdfjsTests.js rename to test/unit/js/pdfjsTests.js index f90c6f49..92f10279 100644 --- a/test/unit/lib/pdfjsTests.js +++ b/test/unit/js/pdfjsTests.js @@ -3,7 +3,7 @@ const Path = require('path') const { expect } = require('chai') const { parseXrefTable } = require('../../../app/lib/pdfjs/parseXrefTable') const PATH_EXAMPLES = 'test/acceptance/fixtures/examples/' -const PATH_SNAPSHOTS = 'test/unit/lib/snapshots/' +const PATH_SNAPSHOTS = 'test/unit/js/snapshots/pdfjs/' const EXAMPLES = fs.readdirSync(PATH_EXAMPLES) function snapshotPath(example) { diff --git a/test/unit/lib/snapshots/asymptote/XrefTable.json b/test/unit/js/snapshots/pdfjs/asymptote/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/asymptote/XrefTable.json rename to test/unit/js/snapshots/pdfjs/asymptote/XrefTable.json diff --git a/test/unit/lib/snapshots/biber_bibliography/XrefTable.json b/test/unit/js/snapshots/pdfjs/biber_bibliography/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/biber_bibliography/XrefTable.json rename to test/unit/js/snapshots/pdfjs/biber_bibliography/XrefTable.json diff --git a/test/unit/lib/snapshots/epstopdf/XrefTable.json b/test/unit/js/snapshots/pdfjs/epstopdf/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/epstopdf/XrefTable.json rename to test/unit/js/snapshots/pdfjs/epstopdf/XrefTable.json diff --git a/test/unit/lib/snapshots/feynmf/XrefTable.json b/test/unit/js/snapshots/pdfjs/feynmf/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/feynmf/XrefTable.json rename to test/unit/js/snapshots/pdfjs/feynmf/XrefTable.json diff --git a/test/unit/lib/snapshots/feynmp/XrefTable.json b/test/unit/js/snapshots/pdfjs/feynmp/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/feynmp/XrefTable.json rename to test/unit/js/snapshots/pdfjs/feynmp/XrefTable.json diff --git a/test/unit/lib/snapshots/fontawesome/XrefTable.json b/test/unit/js/snapshots/pdfjs/fontawesome/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/fontawesome/XrefTable.json rename to test/unit/js/snapshots/pdfjs/fontawesome/XrefTable.json diff --git a/test/unit/lib/snapshots/fontawesome_xelatex/XrefTable.json b/test/unit/js/snapshots/pdfjs/fontawesome_xelatex/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/fontawesome_xelatex/XrefTable.json rename to test/unit/js/snapshots/pdfjs/fontawesome_xelatex/XrefTable.json diff --git a/test/unit/lib/snapshots/glossaries/XrefTable.json b/test/unit/js/snapshots/pdfjs/glossaries/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/glossaries/XrefTable.json rename to test/unit/js/snapshots/pdfjs/glossaries/XrefTable.json diff --git a/test/unit/lib/snapshots/gnuplot/XrefTable.json b/test/unit/js/snapshots/pdfjs/gnuplot/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/gnuplot/XrefTable.json rename to test/unit/js/snapshots/pdfjs/gnuplot/XrefTable.json diff --git a/test/unit/lib/snapshots/hebrew/XrefTable.json b/test/unit/js/snapshots/pdfjs/hebrew/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/hebrew/XrefTable.json rename to test/unit/js/snapshots/pdfjs/hebrew/XrefTable.json diff --git a/test/unit/lib/snapshots/knitr/XrefTable.json b/test/unit/js/snapshots/pdfjs/knitr/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/knitr/XrefTable.json rename to test/unit/js/snapshots/pdfjs/knitr/XrefTable.json diff --git a/test/unit/lib/snapshots/knitr_utf8/XrefTable.json b/test/unit/js/snapshots/pdfjs/knitr_utf8/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/knitr_utf8/XrefTable.json rename to test/unit/js/snapshots/pdfjs/knitr_utf8/XrefTable.json diff --git a/test/unit/lib/snapshots/latex_compiler/XrefTable.json b/test/unit/js/snapshots/pdfjs/latex_compiler/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/latex_compiler/XrefTable.json rename to test/unit/js/snapshots/pdfjs/latex_compiler/XrefTable.json diff --git a/test/unit/lib/snapshots/lualatex_compiler/XrefTable.json b/test/unit/js/snapshots/pdfjs/lualatex_compiler/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/lualatex_compiler/XrefTable.json rename to test/unit/js/snapshots/pdfjs/lualatex_compiler/XrefTable.json diff --git a/test/unit/lib/snapshots/makeindex-custom-style/XrefTable.json b/test/unit/js/snapshots/pdfjs/makeindex-custom-style/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/makeindex-custom-style/XrefTable.json rename to test/unit/js/snapshots/pdfjs/makeindex-custom-style/XrefTable.json diff --git a/test/unit/lib/snapshots/makeindex/XrefTable.json b/test/unit/js/snapshots/pdfjs/makeindex/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/makeindex/XrefTable.json rename to test/unit/js/snapshots/pdfjs/makeindex/XrefTable.json diff --git a/test/unit/lib/snapshots/minted/XrefTable.json b/test/unit/js/snapshots/pdfjs/minted/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/minted/XrefTable.json rename to test/unit/js/snapshots/pdfjs/minted/XrefTable.json diff --git a/test/unit/lib/snapshots/multibib_bibliography/XrefTable.json b/test/unit/js/snapshots/pdfjs/multibib_bibliography/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/multibib_bibliography/XrefTable.json rename to test/unit/js/snapshots/pdfjs/multibib_bibliography/XrefTable.json diff --git a/test/unit/lib/snapshots/nomenclature/XrefTable.json b/test/unit/js/snapshots/pdfjs/nomenclature/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/nomenclature/XrefTable.json rename to test/unit/js/snapshots/pdfjs/nomenclature/XrefTable.json diff --git a/test/unit/lib/snapshots/references_in_include/XrefTable.json b/test/unit/js/snapshots/pdfjs/references_in_include/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/references_in_include/XrefTable.json rename to test/unit/js/snapshots/pdfjs/references_in_include/XrefTable.json diff --git a/test/unit/lib/snapshots/simple_bibliography/XrefTable.json b/test/unit/js/snapshots/pdfjs/simple_bibliography/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/simple_bibliography/XrefTable.json rename to test/unit/js/snapshots/pdfjs/simple_bibliography/XrefTable.json diff --git a/test/unit/lib/snapshots/subdirectories/XrefTable.json b/test/unit/js/snapshots/pdfjs/subdirectories/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/subdirectories/XrefTable.json rename to test/unit/js/snapshots/pdfjs/subdirectories/XrefTable.json diff --git a/test/unit/lib/snapshots/tikz_feynman/XrefTable.json b/test/unit/js/snapshots/pdfjs/tikz_feynman/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/tikz_feynman/XrefTable.json rename to test/unit/js/snapshots/pdfjs/tikz_feynman/XrefTable.json diff --git a/test/unit/lib/snapshots/xelatex_compiler/XrefTable.json b/test/unit/js/snapshots/pdfjs/xelatex_compiler/XrefTable.json similarity index 100% rename from test/unit/lib/snapshots/xelatex_compiler/XrefTable.json rename to test/unit/js/snapshots/pdfjs/xelatex_compiler/XrefTable.json From 06d24e08ce5bbafd00b9d4dca18049b9f91d6263 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <mans0954@users.noreply.github.com> Date: Thu, 16 Dec 2021 16:35:03 +0000 Subject: [PATCH 709/709] [misc] document monorepo migration --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 302cb349..01e0074b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +⚠️ This repository has been migrated into [`overleaf/overleaf`](https://github.com/overleaf/overleaf). See the [monorepo announcement](https://github.com/overleaf/overleaf/issues/923) for more info. ⚠️ + +--- + overleaf/clsi ===============