app.coffee 8.2 KB

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