app.coffee 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. CompileController = require "./app/js/CompileController"
  2. Settings = require "settings-sharelatex"
  3. logger = require "logger-sharelatex"
  4. logger.initialize("clsi")
  5. if Settings.sentry?.dsn?
  6. logger.initializeErrorReporting(Settings.sentry.dsn)
  7. smokeTest = require "smoke-test-sharelatex"
  8. ContentTypeMapper = require "./app/js/ContentTypeMapper"
  9. Path = require "path"
  10. fs = require "fs"
  11. Metrics = require "metrics-sharelatex"
  12. Metrics.initialize("clsi")
  13. Metrics.open_sockets.monitor(logger)
  14. Metrics.memory.monitor(logger)
  15. ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager"
  16. OutputCacheManager = require "./app/js/OutputCacheManager"
  17. require("./app/js/db").sync()
  18. express = require "express"
  19. bodyParser = require "body-parser"
  20. app = express()
  21. app.use Metrics.http.monitor(logger)
  22. # Compile requests can take longer than the default two
  23. # minutes (including file download time), so bump up the
  24. # timeout a bit.
  25. TIMEOUT = 6 * 60 * 1000
  26. app.use (req, res, next) ->
  27. req.setTimeout TIMEOUT
  28. res.setTimeout TIMEOUT
  29. next()
  30. app.param 'project_id', (req, res, next, project_id) ->
  31. if project_id?.match /^[a-zA-Z0-9_-]+$/
  32. next()
  33. else
  34. next new Error("invalid project id")
  35. app.param 'user_id', (req, res, next, user_id) ->
  36. if user_id?.match /^[0-9a-f]{24}$/
  37. next()
  38. else
  39. next new Error("invalid user id")
  40. app.param 'build_id', (req, res, next, build_id) ->
  41. if build_id?.match OutputCacheManager.BUILD_REGEX
  42. next()
  43. else
  44. next new Error("invalid build id #{build_id}")
  45. app.post "/project/:project_id/compile", bodyParser.json(limit: "5mb"), CompileController.compile
  46. app.post "/project/:project_id/compile/stop", CompileController.stopCompile
  47. app.delete "/project/:project_id", CompileController.clearCache
  48. app.get "/project/:project_id/sync/code", CompileController.syncFromCode
  49. app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf
  50. app.get "/project/:project_id/wordcount", CompileController.wordcount
  51. app.get "/project/:project_id/status", CompileController.status
  52. # Per-user containers
  53. app.post "/project/:project_id/user/:user_id/compile", bodyParser.json(limit: "5mb"), CompileController.compile
  54. app.post "/project/:project_id/user/:user_id/compile/stop", CompileController.stopCompile
  55. app.delete "/project/:project_id/user/:user_id", CompileController.clearCache
  56. app.get "/project/:project_id/user/:user_id/sync/code", CompileController.syncFromCode
  57. app.get "/project/:project_id/user/:user_id/sync/pdf", CompileController.syncFromPdf
  58. app.get "/project/:project_id/user/:user_id/wordcount", CompileController.wordcount
  59. ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks"
  60. # create a static server which does not allow access to any symlinks
  61. # avoids possible mismatch of root directory between middleware check
  62. # and serving the files
  63. staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHeaders: (res, path, stat) ->
  64. if Path.basename(path) == "output.pdf"
  65. # Calculate an etag in the same way as nginx
  66. # https://github.com/tj/send/issues/65
  67. etag = (path, stat) ->
  68. '"' + Math.ceil(+stat.mtime / 1000).toString(16) +
  69. '-' + Number(stat.size).toString(16) + '"'
  70. res.set("Etag", etag(path, stat))
  71. res.set("Content-Type", ContentTypeMapper.map(path))
  72. app.get "/project/:project_id/user/:user_id/build/:build_id/output/*", (req, res, next) ->
  73. # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
  74. req.url = "/#{req.params.project_id}-#{req.params.user_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}")
  75. staticServer(req, res, next)
  76. app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) ->
  77. # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
  78. req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.params.build_id, "/#{req.params[0]}")
  79. staticServer(req, res, next)
  80. app.get "/project/:project_id/user/:user_id/output/*", (req, res, next) ->
  81. # for specific user get the path to the top level file
  82. req.url = "/#{req.params.project_id}-#{req.params.user_id}/#{req.params[0]}"
  83. staticServer(req, res, next)
  84. app.get "/project/:project_id/output/*", (req, res, next) ->
  85. if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX)
  86. # for specific build get the path from the OutputCacheManager (e.g. .clsi/buildId)
  87. req.url = "/#{req.params.project_id}/" + OutputCacheManager.path(req.query.build, "/#{req.params[0]}")
  88. else
  89. req.url = "/#{req.params.project_id}/#{req.params[0]}"
  90. staticServer(req, res, next)
  91. app.get "/oops", (req, res, next) ->
  92. logger.error {err: "hello"}, "test error"
  93. res.send "error\n"
  94. app.get "/status", (req, res, next) ->
  95. res.send "CLSI is alive\n"
  96. resCacher =
  97. contentType:(@setContentType)->
  98. send:(@code, @body)->
  99. #default the server to be down
  100. code:500
  101. body:{}
  102. setContentType:"application/json"
  103. if Settings.smokeTest
  104. do runSmokeTest = ->
  105. logger.log("running smoke tests")
  106. smokeTest.run(require.resolve(__dirname + "/test/smoke/js/SmokeTests.js"))({}, resCacher)
  107. setTimeout(runSmokeTest, 20 * 1000)
  108. app.get "/health_check", (req, res)->
  109. res.contentType(resCacher?.setContentType)
  110. res.status(resCacher?.code).send(resCacher?.body)
  111. profiler = require "v8-profiler"
  112. app.get "/profile", (req, res) ->
  113. time = parseInt(req.query.time || "1000")
  114. profiler.startProfiling("test")
  115. setTimeout () ->
  116. profile = profiler.stopProfiling("test")
  117. res.json(profile)
  118. , time
  119. app.get "/heapdump", (req, res)->
  120. require('heapdump').writeSnapshot '/tmp/' + Date.now() + '.clsi.heapsnapshot', (err, filename)->
  121. res.send filename
  122. app.use (error, req, res, next) ->
  123. logger.error err: error, "server error"
  124. res.sendStatus(error?.statusCode || 500)
  125. app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) ->
  126. logger.info "CLSI starting up, listening on #{host}:#{port}"
  127. setInterval () ->
  128. ProjectPersistenceManager.clearExpiredProjects()
  129. , tenMinutes = 10 * 60 * 1000
  130. net = require('net')
  131. os = require('os')
  132. server = net.createServer (socket) ->
  133. socket.on "error", (err)->
  134. if err.code == "ECONNRESET"
  135. # this always comes up, we don't know why
  136. return
  137. logger.err err:err, "error with socket on load check"
  138. socket.destroy()
  139. currentLoad = os.loadavg()[0]
  140. availableWorkingCpus = os.cpus().length - 1
  141. freeLoad = availableWorkingCpus - currentLoad
  142. freeLoadPercentage = Math.round((freeLoad / availableWorkingCpus) * 100)
  143. if freeLoadPercentage <= 0
  144. freeLoadPercentage = 1 # when its 0 the server is set to drain and will move projects to different servers
  145. socket.write("up, #{freeLoadPercentage}%\n", "ASCII")
  146. socket.end()
  147. server.listen load_port = (Settings.internal?.clsi?.load_port or 3044), ->
  148. logger.info "tcp load endpoint listening on port #{load_port}"
  149. # telnet 127.0.0.1 3044