diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..386f26d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules/* +gitrev +.git +.gitignore +.npm +.nvmrc +nodemon.json +app.js +**/js/* diff --git a/.gitignore b/.gitignore index 99e9760..7fb78ee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,7 @@ app.js cache .vagrant db.sqlite +db.sqlite-wal +db.sqlite-shm config/* -bin/synctex +npm-debug.log diff --git a/.nvmrc b/.nvmrc index e18a34b..bbf0c5a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.11.2 +6.14.1 diff --git a/.viminfo b/.viminfo new file mode 100644 index 0000000..78c0129 --- /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 new file mode 100644 index 0000000..1ccc689 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:6.14.1 as app + +WORKDIR /app + +#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.14.1 + +COPY --from=app /app /app + +WORKDIR /app +RUN chmod 0755 ./install_deps.sh && ./install_deps.sh +ENTRYPOINT ["/bin/sh", "entrypoint.sh"] + +CMD ["node","app.js"] diff --git a/Jenkinsfile b/Jenkinsfile index ce7367d..d82360d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,79 +1,69 @@ -pipeline { +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('@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' - args "-v /var/lib/jenkins/.npm:/tmp/.npm -e HOME=/tmp" - reuseNode true + 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 '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 'make build' } } - stage('Compile and Test') { - agent { - docker { - image 'node:6.11.2' - reuseNode true - } - } + + stage('Unit Tests') { 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 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit' } } + stage('Acceptance Tests') { - environment { - TEXLIVE_IMAGE="quay.io/sharelatex/texlive-full:2017.1" - } steps { - sh 'mkdir -p compiles cache' - // Not yet running, due to volumes/sibling containers - 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' - // 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 .' + + 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') { + + 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,11 +72,37 @@ pipeline { } 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" + } } } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b1ce293 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +# 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 + +BUILD_NUMBER ?= local +BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +PROJECT_NAME = clsi +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: + 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 + +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 + @[ ! -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 ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + . + +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/app.coffee b/app.coffee index 5c79b8e..8b5c779 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) -> @@ -139,6 +140,14 @@ 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) + + +#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") @@ -160,8 +169,76 @@ 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}" +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 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 + + +port = (Settings.internal?.clsi?.port or 3013) +host = (Settings.internal?.clsi?.host or "localhost") + +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}" + + 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, (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/app/coffee/CommandRunner.coffee b/app/coffee/CommandRunner.coffee index f47af00..2d1c3a9 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/CompileController.coffee b/app/coffee/CompileController.coffee index 1d90405..5d1ee2e 100644 --- a/app/coffee/CompileController.coffee +++ b/app/coffee/CompileController.coffee @@ -34,11 +34,16 @@ 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" + # log an error if any core files are found for file in outputFiles if file.path is "core" @@ -77,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 { @@ -90,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 b0573d1..eff20eb 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 @@ -41,7 +38,6 @@ module.exports = CompileManager = 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) -> @@ -205,21 +201,31 @@ 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") - CompileManager._runSynctex ["code", synctex_path, file_path, line, column], (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" - callback null, CompileManager._parseSynctexFromCodeOutput(stdout) + 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) - 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, user_id:user_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output" - callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir) + 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) @@ -235,19 +241,19 @@ 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 - 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) + + 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 = [] @@ -276,23 +282,28 @@ 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) - - 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? - logger.err 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 = { diff --git a/app/coffee/DbQueue.coffee b/app/coffee/DbQueue.coffee new file mode 100644 index 0000000..0e02590 --- /dev/null +++ b/app/coffee/DbQueue.coffee @@ -0,0 +1,13 @@ +async = require "async" +Settings = require "settings-sharelatex" + +queue = async.queue((task, cb)-> + task(cb) + , Settings.parallelSqlQueryLimit) + +queue.drain = ()-> + console.log('HI all items have been processed') + +module.exports = + queue: queue + diff --git a/app/coffee/DockerLockManager.coffee b/app/coffee/DockerLockManager.coffee new file mode 100644 index 0000000..739f2cd --- /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 0000000..3c2ed9c --- /dev/null +++ b/app/coffee/DockerRunner.coffee @@ -0,0 +1,358 @@ +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/' + # ... becomes ... + # '/opt/sharelatex_data/data/compiles/' + 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? + + 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 = + "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.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) -> + 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 6a5a4f6..3571af2 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 0000000..f47af00 --- /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/app/coffee/LockManager.coffee b/app/coffee/LockManager.coffee index 5d6fa46..afa3cca 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 @@ -14,10 +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' - return callback(error) if error? - runner (error1, args...) -> - Lockfile.unlock path, (error2) -> - error = error1 or error2 - return callback(error) if error? - callback(null, args...) + 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...) diff --git a/app/coffee/OutputFileFinder.coffee b/app/coffee/OutputFileFinder.coffee index 4b07f6e..662440b 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/ProjectPersistenceManager.coffee b/app/coffee/ProjectPersistenceManager.coffee index 403043f..4ea02bf 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,14 +12,17 @@ module.exports = ProjectPersistenceManager = EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5 markProjectAsJustAccessed: (project_id, callback = (error) ->) -> - db.Project.findOrCreate(where: {project_id: project_id}) - .spread( - (project, created) -> - project.updateAttributes(lastAccessed: new Date()) - .then(() -> callback()) - .error callback - ) - .error callback + job = (cb)-> + 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) -> @@ -47,20 +51,34 @@ 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) ->) -> - db.Project.destroy(where: {project_id: project_id}) - .then(() -> callback()) - .error callback + 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) + _findExpiredProjectIds: (callback = (error, project_ids) ->) -> - 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)-> + 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 + + dbQueue.queue.push(job, callback) + logger.log {EXPIRY_TIMEOUT: ProjectPersistenceManager.EXPIRY_TIMEOUT}, "project assets kept timeout" diff --git a/app/coffee/RequestParser.coffee b/app/coffee/RequestParser.coffee index 596b529..cabbac3 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 diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 0b6aef5..0c9f718 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -120,7 +120,11 @@ 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") fs.writeFile path, resource.content, callback + try + result = fs.lstatSync(path) + catch e checkPath: (basePath, resourcePath, callback) -> path = Path.normalize(Path.join(basePath, resourcePath)) diff --git a/app/coffee/UrlCache.coffee b/app/coffee/UrlCache.coffee index b72b78c..d44479a 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") @@ -51,7 +52,6 @@ module.exports = UrlCache = _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() @@ -94,32 +94,41 @@ module.exports = UrlCache = return callback() _findUrlDetails: (project_id, url, callback = (error, urlDetails) ->) -> - 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 + dbQueue.queue.push job, callback _updateOrCreateUrlDetails: (project_id, url, lastModified, callback = (error) ->) -> - 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)-> + db.UrlCache.findOrCreate(where: {url: url, project_id: project_id}) + .spread( + (urlDetails, created) -> + urlDetails.updateAttributes(lastModified: lastModified) + .then(() -> cb()) + .error(cb) + ) + .error cb + dbQueue.queue.push(job, callback) _clearUrlDetails: (project_id, url, callback = (error) ->) -> - db.UrlCache.destroy(where: {url: url, project_id: project_id}) - .then(() -> callback null) - .error callback + job = (cb)-> + db.UrlCache.destroy(where: {url: url, project_id: project_id}) + .then(() -> cb null) + .error cb + dbQueue.queue.push(job, callback) + _findAllUrlsInProject: (project_id, callback = (error, urls) ->) -> - db.UrlCache.findAll(where: { project_id: project_id }) - .then( - (urlEntries) -> - callback null, urlEntries.map((entry) -> entry.url) - ) - .error callback + job = (cb)-> + db.UrlCache.findAll(where: { project_id: project_id }) + .then( + (urlEntries) -> + cb null, urlEntries.map((entry) -> entry.url) + ) + .error cb + dbQueue.queue.push(job, callback) diff --git a/app/coffee/UrlFetcher.coffee b/app/coffee/UrlFetcher.coffee index 201306c..da10859 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" diff --git a/app/coffee/db.coffee b/app/coffee/db.coffee index a72f61e..de48dfd 100644 --- a/app/coffee/db.coffee +++ b/app/coffee/db.coffee @@ -1,9 +1,12 @@ Sequelize = require("sequelize") Settings = require("settings-sharelatex") _ = require("underscore") +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, @@ -11,6 +14,12 @@ sequelize = new Sequelize( 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;") + module.exports = UrlCache: sequelize.define("UrlCache", { url: Sequelize.STRING @@ -32,5 +41,15 @@ module.exports = ] }) - sync: () -> sequelize.sync() + 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" + ) + diff --git a/bin/acceptance_test b/bin/acceptance_test new file mode 100644 index 0000000..fd2e513 --- /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/bin/install_texlive_gce.sh b/bin/install_texlive_gce.sh new file mode 100755 index 0000000..ee6efba --- /dev/null +++ b/bin/install_texlive_gce.sh @@ -0,0 +1,21 @@ +#!/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/bin/synctex b/bin/synctex new file mode 100755 index 0000000..89b8cc6 Binary files /dev/null and b/bin/synctex differ diff --git a/config/settings.defaults.coffee b/config/settings.defaults.coffee index 0e8f6aa..88f4532 100644 --- a/config/settings.defaults.coffee +++ b/config/settings.defaults.coffee @@ -7,9 +7,13 @@ module.exports = clsi: database: "clsi" username: "clsi" - password: null dialect: "sqlite" - storage: Path.resolve(__dirname + "/../db.sqlite") + storage: process.env["SQLITE_PATH"] or Path.resolve(__dirname + "/../db.sqlite") + pool: + max: 1 + min: 1 + retry: + max: 10 path: compilesDir: Path.resolve(__dirname + "/../compiles") @@ -20,19 +24,28 @@ 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://localhost:3013" + 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 + 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: + dsn: process.env['SENTRY_DSN'] -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: @@ -41,4 +54,15 @@ if process.env["COMMAND_RUNNER"] user: process.env["TEXLIVE_IMAGE_USER"] or "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}" + + module.exports.path.synctexBaseDir = -> "/compile" + module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"] + + module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"] diff --git a/debug b/debug new file mode 100755 index 0000000..fcc371c --- /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-config.yml b/docker-compose-config.yml new file mode 100644 index 0000000..c8b7dcc --- /dev/null +++ b/docker-compose-config.yml @@ -0,0 +1,32 @@ +version: "2" + +services: + dev: + environment: + TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 + TEXLIVE_IMAGE_USER: "tex" + SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + DOCKER_RUNNER: "true" + COMPILES_HOST_DIR: $PWD/compiles + SYNCTEX_BIN_HOST_PATH: $PWD/bin/synctex + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./compiles:/app/compiles + - ./cache:/app/cache + - ./bin/synctex:/app/bin/synctex + + + ci: + environment: + TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2017.1 + TEXLIVE_IMAGE_USER: "tex" + SHARELATEX_CONFIG: /app/config/settings.defaults.coffee + 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 + - ./cache:/app/cache + - ./bin/synctex:/app/bin/synctex diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 0000000..f6c8a27 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,34 @@ +# 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: "2" + +services: + test_unit: + 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 + 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 + command: 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 0000000..371e6e7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +# 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: "2" + +services: + test_unit: + build: . + volumes: + - .:/app + working_dir: /app + environment: + MOCHA_GREP: ${MOCHA_GREP} + command: npm run test:unit + + test_acceptance: + build: . + volumes: + - .:/app + working_dir: /app + extends: + file: docker-compose-config.yml + service: dev + environment: + ELASTIC_SEARCH_DSN: es:9200 + REDIS_HOST: redis + MONGO_HOST: mongo + POSTGRES_HOST: postgres + MOCHA_GREP: ${MOCHA_GREP} + depends_on: + - mongo + - redis + command: npm run test:acceptance + + redis: + image: redis + + mongo: + image: mongo:3.4 + diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..cb2580c --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,23 @@ +#!/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 +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 + +chown -R node:node /app/bin + +./bin/install_texlive_gce.sh +exec runuser -u node "$@" \ No newline at end of file diff --git a/install_deps.sh b/install_deps.sh new file mode 100755 index 0000000..49bdc5c --- /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/kube.yaml b/kube.yaml new file mode 100644 index 0000000..d3fb042 --- /dev/null +++ b/kube.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Service +metadata: + name: clsi + namespace: default +spec: + type: LoadBalancer + ports: + - port: 80 + protocol: TCP + targetPort: 80 + 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 + imagePullPolicy: Always + readinessProbe: + httpGet: + path: status + port: 80 + periodSeconds: 5 + initialDelaySeconds: 0 + failureThreshold: 3 + successThreshold: 1 + + + diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..98db38d --- /dev/null +++ b/nodemon.json @@ -0,0 +1,19 @@ +{ + "ignore": [ + ".git", + "node_modules/" + ], + "verbose": true, + "legacyWatch": true, + "execMap": { + "js": "npm run start" + }, + + "watch": [ + "app/coffee/", + "app.coffee", + "config/" + ], + "ext": "coffee" + +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d58c1d7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3002 @@ +{ + "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=" + } + } +} diff --git a/package.json b/package.json index 867bdf2..f2183ef 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,23 @@ "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" + "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", + "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", + "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" }, "author": "James Allen ", "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", @@ -21,20 +31,20 @@ "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", "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.8", + "sqlite3": "^4.0.2", "underscore": "^1.8.2", "v8-profiler": "^5.2.4", "wrench": "~1.5.4" }, "devDependencies": { - "mocha": "1.10.0", + "mocha": "^4.0.1", "coffee-script": "1.6.0", "chai": "~1.8.1", "sinon": "~1.7.3", diff --git a/patch-texlive-dockerfile b/patch-texlive-dockerfile new file mode 100644 index 0000000..61cb796 --- /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/seccomp/clsi-profile.json b/seccomp/clsi-profile.json new file mode 100644 index 0000000..34fd252 --- /dev/null +++ b/seccomp/clsi-profile.json @@ -0,0 +1,832 @@ +{ + "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": "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", + "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": "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", + "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 diff --git a/synctex.profile b/synctex.profile new file mode 100644 index 0000000..577a901 --- /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 + + diff --git a/test/acceptance/coffee/BrokenLatexFileTests.coffee b/test/acceptance/coffee/BrokenLatexFileTests.coffee index 5a92d5f..8ab4344 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) -> diff --git a/test/acceptance/coffee/DeleteOldFilesTest.coffee b/test/acceptance/coffee/DeleteOldFilesTest.coffee index b8a1ff3..1cb6776 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 4d431c2..d4bd19f 100644 --- a/test/acceptance/coffee/ExampleDocumentTests.coffee +++ b/test/acceptance/coffee/ExampleDocumentTests.coffee @@ -3,15 +3,23 @@ request = require "request" require("chai").should() fs = require "fs" ChildProcess = require "child_process" - -fixturePath = (path) -> __dirname + "/../fixtures/" + path - +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 e +catch err + console.log err, fixturePath("tmp"), "unable to create fixture tmp path" convertToPng = (pdfPath, pngPath, callback = (error) ->) -> - convert = ChildProcess.exec "convert #{fixturePath(pdfPath)} #{fixturePath(pngPath)}" + 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() @@ -40,7 +48,6 @@ checkPdfInfo = (pdfPath, callback = (error, output) ->) -> if stdout.match(/Optimized:\s+yes/) callback null, true else - console.log "pdfinfo result", stdout callback null, false compareMultiplePages = (project_id, callback = (error) ->) -> @@ -57,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) => @@ -75,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? @@ -85,7 +95,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 2693f63..95b667b 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 02b2397..685d292 100644 --- a/test/acceptance/coffee/SynctexTests.coffee +++ b/test/acceptance/coffee/SynctexTests.coffee @@ -2,21 +2,25 @@ Client = require "./helpers/Client" 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() - 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) -> @@ -29,7 +33,7 @@ describe "Syncing", -> 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 5e0058d..c3acd8f 100644 --- a/test/acceptance/coffee/TimeoutTests.coffee +++ b/test/acceptance/coffee/TimeoutTests.coffee @@ -1,24 +1,27 @@ 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 + timeout: 10 #seconds resources: [ path: "main.tex" content: ''' \\documentclass{article} \\begin{document} - Hello world - \\input{|"sleep 10"} + \\def\\x{Hello!\\par\\x} + \\x \\end{document} ''' ] @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 9e6f3d6..cef7446 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 d84ecba..abace06 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/Client.coffee b/test/acceptance/coffee/helpers/Client.coffee index c67b425..3913170 100644 --- a/test/acceptance/coffee/helpers/Client.coffee +++ b/test/acceptance/coffee/helpers/Client.coffee @@ -30,6 +30,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) diff --git a/test/acceptance/coffee/helpers/ClsiApp.coffee b/test/acceptance/coffee/helpers/ClsiApp.coffee new file mode 100644 index 0000000..d9cd534 --- /dev/null +++ b/test/acceptance/coffee/helpers/ClsiApp.coffee @@ -0,0 +1,24 @@ +app = require('../../../../app') +require("logger-sharelatex").logger.level("info") +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 diff --git a/test/unit/coffee/CompileControllerTests.coffee b/test/unit/coffee/CompileControllerTests.coffee index 7b6001d..f0269ee 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 14ddb2e..608a3e5 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,23 @@ 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 + @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, + 60000, + {} + ).should.equal true it "should call the callback with the parsed output", -> @callback @@ -272,15 +287,21 @@ 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 + @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, + 60000, + {}).should.equal true it "should call the callback with the parsed output", -> @callback @@ -297,7 +318,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 = 60 * 1000 @file_name = "main.tex" @Settings.path.compilesDir = "/local/compile/directory" diff --git a/test/unit/coffee/DockerLockManagerTests.coffee b/test/unit/coffee/DockerLockManagerTests.coffee new file mode 100644 index 0000000..6161bec --- /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 0000000..307ffde --- /dev/null +++ b/test/unit/coffee/DockerRunnerTests.coffee @@ -0,0 +1,509 @@ +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 "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 + .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 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 91% rename from test/unit/coffee/LockManager.coffee rename to test/unit/coffee/LockManagerTests.coffee index c1071a5..9dd1d46 100644 --- a/test/unit/coffee/LockManager.coffee +++ b/test/unit/coffee/LockManagerTests.coffee @@ -5,11 +5,14 @@ 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": {} - "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" diff --git a/test/unit/coffee/RequestParserTests.coffee b/test/unit/coffee/RequestParserTests.coffee index 0b420b3..f63bc55 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,13 @@ 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 "without a timeout specified", -> beforeEach -> delete @validRequest.compile.options.timeout diff --git a/test/unit/coffee/UrlFetcherTests.coffee b/test/unit/coffee/UrlFetcherTests.coffee index dd709dd..e91720e 100644 --- a/test/unit/coffee/UrlFetcherTests.coffee +++ b/test/unit/coffee/UrlFetcherTests.coffee @@ -7,17 +7,18 @@ 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 "_pipeUrlToFile", -> + + describe "rewrite url domain if filestoreDomainOveride is set", -> beforeEach -> @path = "/path/to/file/on/disk" @request.get = sinon.stub().returns(@urlStream = new EventEmitter) @@ -26,21 +27,54 @@ 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 + 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 -> + 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 +89,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 +103,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", ->