Просмотр исходного кода

Merge pull request #86 from sharelatex/ho-dockerise

Dockerised clsi
Henry Oswald 8 лет назад
Родитель
Сommit
e73fb261c2
58 измененных файлов с 5891 добавлено и 256 удалено
  1. 9 0
      services/clsi/.dockerignore
  2. 3 1
      services/clsi/.gitignore
  3. 1 1
      services/clsi/.nvmrc
  4. 35 0
      services/clsi/.viminfo
  5. 23 0
      services/clsi/Dockerfile
  6. 66 50
      services/clsi/Jenkinsfile
  7. 47 0
      services/clsi/Makefile
  8. 79 2
      services/clsi/app.coffee
  9. 8 41
      services/clsi/app/coffee/CommandRunner.coffee
  10. 5 2
      services/clsi/app/coffee/CompileController.coffee
  11. 51 40
      services/clsi/app/coffee/CompileManager.coffee
  12. 13 0
      services/clsi/app/coffee/DbQueue.coffee
  13. 56 0
      services/clsi/app/coffee/DockerLockManager.coffee
  14. 358 0
      services/clsi/app/coffee/DockerRunner.coffee
  15. 1 1
      services/clsi/app/coffee/LatexRunner.coffee
  16. 44 0
      services/clsi/app/coffee/LocalCommandRunner.coffee
  17. 16 8
      services/clsi/app/coffee/LockManager.coffee
  18. 0 2
      services/clsi/app/coffee/OutputFileFinder.coffee
  19. 36 18
      services/clsi/app/coffee/ProjectPersistenceManager.coffee
  20. 2 0
      services/clsi/app/coffee/RequestParser.coffee
  21. 4 0
      services/clsi/app/coffee/ResourceWriter.coffee
  22. 30 21
      services/clsi/app/coffee/UrlCache.coffee
  23. 5 0
      services/clsi/app/coffee/UrlFetcher.coffee
  24. 20 1
      services/clsi/app/coffee/db.coffee
  25. 4 0
      services/clsi/bin/acceptance_test
  26. 21 0
      services/clsi/bin/install_texlive_gce.sh
  27. BIN
      services/clsi/bin/synctex
  28. 33 9
      services/clsi/config/settings.defaults.coffee
  29. 5 0
      services/clsi/debug
  30. 32 0
      services/clsi/docker-compose-config.yml
  31. 34 0
      services/clsi/docker-compose.ci.yml
  32. 42 0
      services/clsi/docker-compose.yml
  33. 23 0
      services/clsi/entrypoint.sh
  34. 4 0
      services/clsi/install_deps.sh
  35. 41 0
      services/clsi/kube.yaml
  36. 19 0
      services/clsi/nodemon.json
  37. 3002 0
      services/clsi/package-lock.json
  38. 16 6
      services/clsi/package.json
  39. 3 0
      services/clsi/patch-texlive-dockerfile
  40. 832 0
      services/clsi/seccomp/clsi-profile.json
  41. 34 0
      services/clsi/synctex.profile
  42. 3 1
      services/clsi/test/acceptance/coffee/BrokenLatexFileTests.coffee
  43. 4 2
      services/clsi/test/acceptance/coffee/DeleteOldFilesTest.coffee
  44. 19 7
      services/clsi/test/acceptance/coffee/ExampleDocumentTests.coffee
  45. 3 1
      services/clsi/test/acceptance/coffee/SimpleLatexFileTests.coffee
  46. 10 6
      services/clsi/test/acceptance/coffee/SynctexTests.coffee
  47. 7 4
      services/clsi/test/acceptance/coffee/TimeoutTests.coffee
  48. 3 1
      services/clsi/test/acceptance/coffee/UrlCachingTests.coffee
  49. 3 1
      services/clsi/test/acceptance/coffee/WordcountTests.coffee
  50. 1 0
      services/clsi/test/acceptance/coffee/helpers/Client.coffee
  51. 24 0
      services/clsi/test/acceptance/coffee/helpers/ClsiApp.coffee
  52. 1 1
      services/clsi/test/unit/coffee/CompileControllerTests.coffee
  53. 36 15
      services/clsi/test/unit/coffee/CompileManagerTests.coffee
  54. 145 0
      services/clsi/test/unit/coffee/DockerLockManagerTests.coffee
  55. 509 0
      services/clsi/test/unit/coffee/DockerRunnerTests.coffee
  56. 5 2
      services/clsi/test/unit/coffee/LockManagerTests.coffee
  57. 10 1
      services/clsi/test/unit/coffee/RequestParserTests.coffee
  58. 51 11
      services/clsi/test/unit/coffee/UrlFetcherTests.coffee

+ 9 - 0
services/clsi/.dockerignore

@@ -0,0 +1,9 @@
+node_modules/*
+gitrev
+.git
+.gitignore
+.npm
+.nvmrc
+nodemon.json
+app.js
+**/js/*

+ 3 - 1
services/clsi/.gitignore

@@ -12,5 +12,7 @@ app.js
 cache
 .vagrant
 db.sqlite
+db.sqlite-wal
+db.sqlite-shm
 config/*
-bin/synctex
+npm-debug.log

+ 1 - 1
services/clsi/.nvmrc

@@ -1 +1 @@
-6.11.2
+6.14.1

+ 35 - 0
services/clsi/.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

+ 23 - 0
services/clsi/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"]

+ 66 - 50
services/clsi/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"
+      }
     }
   }
 

+ 47 - 0
services/clsi/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

+ 79 - 2
services/clsi/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()

+ 8 - 41
services/clsi/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

+ 5 - 2
services/clsi/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 {

+ 51 - 40
services/clsi/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 = {

+ 13 - 0
services/clsi/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
+

+ 56 - 0
services/clsi/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...)

+ 358 - 0
services/clsi/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/<project-id>'
+			#   ... becomes ...
+			#   '/opt/sharelatex_data/data/compiles/<project-id>'
+			directory = Path.join(Settings.path.sandboxedCompilesHostDir, Path.basename(directory))
+
+		volumes = {}
+		volumes[directory] = "/compile"
+
+		command = (arg.toString().replace?('$COMPILE_DIR', "/compile") for arg in command)
+		if !image?
+			image = Settings.clsi.docker.image
+
+		if Settings.texliveImageNameOveride?
+			img = image.split("/")
+			image = "#{Settings.texliveImageNameOveride}/#{img[2]}"
+
+		options      = DockerRunner._getContainerOptions(command, image, volumes, timeout, environment)
+		fingerprint  = DockerRunner._fingerprintContainer(options)
+		options.name = name = "project-#{project_id}-#{fingerprint}"
+
+		# logOptions = _.clone(options)
+		# logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging"
+		logger.log project_id: project_id, "running docker container"
+		DockerRunner._runAndWaitForContainer options, volumes, timeout, (error, output) ->
+			if error?.message?.match("HTTP code is 500")
+				logger.log err: error, project_id: project_id, "error running container so destroying and retrying"
+				DockerRunner.destroyContainer name, null, true, (error) ->
+					return callback(error) if error?
+					DockerRunner._runAndWaitForContainer options, volumes, timeout, callback
+			else
+				callback(error, output)
+
+		return name # pass back the container name to allow it to be killed
+
+	kill: (container_id, callback = (error) ->) ->
+		logger.log container_id: container_id, "sending kill signal to container"
+		container = dockerode.getContainer(container_id)
+		container.kill (error) ->
+			if error? and error?.message?.match?(/Cannot kill container .* is not running/)
+				logger.warn err: error, container_id: container_id, "container not running, continuing"
+				error = null
+			if error?
+				logger.error err: error, container_id: container_id, "error killing container"
+				return callback(error)
+			else
+				callback()
+
+	_runAndWaitForContainer: (options, volumes, timeout, _callback = (error, output) ->) ->
+		callback = (args...) ->
+			_callback(args...)
+			# Only call the callback once
+			_callback = () ->
+		
+		name = options.name
+
+		streamEnded = false
+		containerReturned = false
+		output = {}
+
+		callbackIfFinished = () ->
+			if streamEnded and containerReturned
+				callback(null, output)
+
+		attachStreamHandler = (error, _output) ->
+			return callback(error) if error?
+			output = _output
+			streamEnded = true
+			callbackIfFinished()
+
+		DockerRunner.startContainer options, volumes, attachStreamHandler, (error, containerId) ->
+			return callback(error) if error?
+			
+			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()

+ 1 - 1
services/clsi/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)
 

+ 44 - 0
services/clsi/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()

+ 16 - 8
services/clsi/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...)

+ 0 - 2
services/clsi/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"

+ 36 - 18
services/clsi/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"

+ 2 - 0
services/clsi/app/coffee/RequestParser.coffee

@@ -1,3 +1,5 @@
+settings = require("settings-sharelatex")
+
 module.exports = RequestParser =
 	VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
 	MAX_TIMEOUT: 300

+ 4 - 0
services/clsi/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))

+ 30 - 21
services/clsi/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)
 
 		
 		

+ 5 - 0
services/clsi/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"

+ 20 - 1
services/clsi/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"
+			)
+
 	

+ 4 - 0
services/clsi/bin/acceptance_test

@@ -0,0 +1,4 @@
+#!/bin/bash
+set -e;
+MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000"
+$MOCHA "$@"

+ 21 - 0
services/clsi/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"
+
+

BIN
services/clsi/bin/synctex


+ 33 - 9
services/clsi/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"]

+ 5 - 0
services/clsi/debug

@@ -0,0 +1,5 @@
+#!/bin/bash
+echo "hello world"
+sleep 3
+echo "awake"
+/opt/synctex pdf /compile/output.pdf 1 100 200

+ 32 - 0
services/clsi/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

+ 34 - 0
services/clsi/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

+ 42 - 0
services/clsi/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
+

+ 23 - 0
services/clsi/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 "$@"

+ 4 - 0
services/clsi/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

+ 41 - 0
services/clsi/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
+
+
+

+ 19 - 0
services/clsi/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"
+
+}

+ 3002 - 0
services/clsi/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="
+    }
+  }
+}

+ 16 - 6
services/clsi/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 <james@sharelatex.com>",
   "dependencies": {
     "async": "0.2.9",
     "body-parser": "^1.2.0",
+    "dockerode": "^2.5.3",
     "express": "^4.2.0",
     "fs-extra": "^0.16.3",
     "grunt-mkdir": "^1.0.0",
@@ -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",

+ 3 - 0
services/clsi/patch-texlive-dockerfile

@@ -0,0 +1,3 @@
+FROM quay.io/sharelatex/texlive-full:2017.1
+
+# RUN usermod -u 1001 tex

+ 832 - 0
services/clsi/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": []
+        }
+    ]
+}

+ 34 - 0
services/clsi/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
+
+

+ 3 - 1
services/clsi/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) ->

+ 4 - 2
services/clsi/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()

+ 19 - 7
services/clsi/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) ->

+ 3 - 1
services/clsi/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")

+ 10 - 6
services/clsi/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 } ]

+ 7 - 4
services/clsi/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"

+ 3 - 1
services/clsi/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()

+ 3 - 1
services/clsi/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) ->

+ 1 - 0
services/clsi/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)

+ 24 - 0
services/clsi/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()

+ 1 - 1
services/clsi/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 = {}

+ 36 - 15
services/clsi/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"

+ 145 - 0
services/clsi/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

+ 509 - 0
services/clsi/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 <whatever> is not running'
+				@fakeContainer =
+					kill: sinon.stub().callsArgWith(0, @fakeError)
+				@Docker::getContainer = sinon.stub().returns(@fakeContainer)
+
+			it 'should not produce an error', (done) ->
+				@DockerRunner.kill @containerId, (err) =>
+					expect(err).to.equal undefined
+					done()
+
+		describe 'when container.kill produces a legitimate error', (done) ->
+			beforeEach ->
+				@fakeError = new Error('woops')
+				@fakeError.statusCode = 500
+				@fakeError.message = 'Totally legitimate reason to throw an error'
+				@fakeContainer =
+					kill: sinon.stub().callsArgWith(0, @fakeError)
+				@Docker::getContainer = sinon.stub().returns(@fakeContainer)
+
+			it 'should produce an error', (done) ->
+				@DockerRunner.kill @containerId, (err) =>
+					expect(err).to.not.equal undefined
+					expect(err).to.equal @fakeError
+					done()

+ 5 - 2
services/clsi/test/unit/coffee/LockManager.coffee → services/clsi/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"
 

+ 10 - 1
services/clsi/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

+ 51 - 11
services/clsi/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", ->