CompileController.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. const RequestParser = require('./RequestParser')
  2. const CompileManager = require('./CompileManager')
  3. const Settings = require('@overleaf/settings')
  4. const Metrics = require('./Metrics')
  5. const ProjectPersistenceManager = require('./ProjectPersistenceManager')
  6. const logger = require('@overleaf/logger')
  7. const Errors = require('./Errors')
  8. let lastSuccessfulCompileTimestamp = 0
  9. function timeSinceLastSuccessfulCompile() {
  10. return Date.now() - lastSuccessfulCompileTimestamp
  11. }
  12. function compile(req, res, next) {
  13. const timer = new Metrics.Timer('compile-request')
  14. RequestParser.parse(req.body, function (error, request) {
  15. if (error) {
  16. return next(error)
  17. }
  18. timer.opts = request.metricsOpts
  19. request.project_id = req.params.project_id
  20. if (req.params.user_id != null) {
  21. request.user_id = req.params.user_id
  22. }
  23. ProjectPersistenceManager.markProjectAsJustAccessed(
  24. request.project_id,
  25. function (error) {
  26. if (error) {
  27. return next(error)
  28. }
  29. CompileManager.doCompileWithLock(request, (error, result) => {
  30. let { buildId, outputFiles, stats, timings } = result || {}
  31. let code, status
  32. if (outputFiles == null) {
  33. outputFiles = []
  34. }
  35. if (error instanceof Errors.AlreadyCompilingError) {
  36. code = 423 // Http 423 Locked
  37. status = 'compile-in-progress'
  38. } else if (error instanceof Errors.FilesOutOfSyncError) {
  39. code = 409 // Http 409 Conflict
  40. status = 'retry'
  41. logger.warn(
  42. {
  43. projectId: request.project_id,
  44. userId: request.user_id,
  45. },
  46. 'files out of sync, please retry'
  47. )
  48. } else if (
  49. error?.code === 'EPIPE' ||
  50. error instanceof Errors.TooManyCompileRequestsError
  51. ) {
  52. // docker returns EPIPE when shutting down
  53. code = 503 // send 503 Unavailable response
  54. status = 'unavailable'
  55. } else if (error?.terminated) {
  56. status = 'terminated'
  57. } else if (error?.validate) {
  58. status = `validation-${error.validate}`
  59. } else if (error?.timedout) {
  60. status = 'timedout'
  61. logger.debug(
  62. { err: error, projectId: request.project_id },
  63. 'timeout running compile'
  64. )
  65. } else if (error) {
  66. status = 'error'
  67. code = 500
  68. logger.error(
  69. { err: error, projectId: request.project_id },
  70. 'error running compile'
  71. )
  72. } else {
  73. if (
  74. outputFiles.some(
  75. file => file.path === 'output.pdf' && file.size > 0
  76. )
  77. ) {
  78. status = 'success'
  79. lastSuccessfulCompileTimestamp = Date.now()
  80. } else if (request.stopOnFirstError) {
  81. status = 'stopped-on-first-error'
  82. } else {
  83. status = 'failure'
  84. logger.warn(
  85. { projectId: request.project_id, outputFiles },
  86. 'project failed to compile successfully, no output.pdf generated'
  87. )
  88. }
  89. // log an error if any core files are found
  90. if (outputFiles.some(file => file.path === 'core')) {
  91. logger.error(
  92. { projectId: request.project_id, req, outputFiles },
  93. 'core file found in output'
  94. )
  95. }
  96. }
  97. if (error) {
  98. outputFiles = error.outputFiles || []
  99. buildId = error.buildId
  100. }
  101. timer.done()
  102. res.status(code || 200).send({
  103. compile: {
  104. status,
  105. error: error?.message || error,
  106. stats,
  107. timings,
  108. buildId,
  109. outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
  110. outputFiles: outputFiles.map(file => ({
  111. url:
  112. `${Settings.apis.clsi.url}/project/${request.project_id}` +
  113. (request.user_id != null ? `/user/${request.user_id}` : '') +
  114. `/build/${file.build}/output/${file.path}`,
  115. ...file,
  116. })),
  117. },
  118. })
  119. })
  120. }
  121. )
  122. })
  123. }
  124. function stopCompile(req, res, next) {
  125. const { project_id: projectId, user_id: userId } = req.params
  126. CompileManager.stopCompile(projectId, userId, function (error) {
  127. if (error) {
  128. return next(error)
  129. }
  130. res.sendStatus(204)
  131. })
  132. }
  133. function clearCache(req, res, next) {
  134. ProjectPersistenceManager.clearProject(
  135. req.params.project_id,
  136. req.params.user_id,
  137. function (error) {
  138. if (error) {
  139. return next(error)
  140. }
  141. // No content
  142. res.sendStatus(204)
  143. }
  144. )
  145. }
  146. function syncFromCode(req, res, next) {
  147. const { file } = req.query
  148. const line = parseInt(req.query.line, 10)
  149. const column = parseInt(req.query.column, 10)
  150. const { imageName } = req.query
  151. const projectId = req.params.project_id
  152. const userId = req.params.user_id
  153. if (imageName && !_isImageNameAllowed(imageName)) {
  154. return res.status(400).send('invalid image')
  155. }
  156. CompileManager.syncFromCode(
  157. projectId,
  158. userId,
  159. file,
  160. line,
  161. column,
  162. imageName,
  163. function (error, pdfPositions) {
  164. if (error) {
  165. return next(error)
  166. }
  167. res.json({
  168. pdf: pdfPositions,
  169. })
  170. }
  171. )
  172. }
  173. function syncFromPdf(req, res, next) {
  174. const page = parseInt(req.query.page, 10)
  175. const h = parseFloat(req.query.h)
  176. const v = parseFloat(req.query.v)
  177. const { imageName } = req.query
  178. const projectId = req.params.project_id
  179. const userId = req.params.user_id
  180. if (imageName && !_isImageNameAllowed(imageName)) {
  181. return res.status(400).send('invalid image')
  182. }
  183. CompileManager.syncFromPdf(
  184. projectId,
  185. userId,
  186. page,
  187. h,
  188. v,
  189. imageName,
  190. function (error, codePositions) {
  191. if (error) {
  192. return next(error)
  193. }
  194. res.json({
  195. code: codePositions,
  196. })
  197. }
  198. )
  199. }
  200. function wordcount(req, res, next) {
  201. const file = req.query.file || 'main.tex'
  202. const projectId = req.params.project_id
  203. const userId = req.params.user_id
  204. const { image } = req.query
  205. if (image && !_isImageNameAllowed(image)) {
  206. return res.status(400).send('invalid image')
  207. }
  208. logger.debug({ image, file, projectId }, 'word count request')
  209. CompileManager.wordcount(
  210. projectId,
  211. userId,
  212. file,
  213. image,
  214. function (error, result) {
  215. if (error) {
  216. return next(error)
  217. }
  218. res.json({
  219. texcount: result,
  220. })
  221. }
  222. )
  223. }
  224. function status(req, res, next) {
  225. res.send('OK')
  226. }
  227. function _isImageNameAllowed(imageName) {
  228. const ALLOWED_IMAGES =
  229. Settings.clsi && Settings.clsi.docker && Settings.clsi.docker.allowedImages
  230. return !ALLOWED_IMAGES || ALLOWED_IMAGES.includes(imageName)
  231. }
  232. module.exports = {
  233. compile,
  234. stopCompile,
  235. clearCache,
  236. syncFromCode,
  237. syncFromPdf,
  238. wordcount,
  239. status,
  240. timeSinceLastSuccessfulCompile,
  241. }