CompileController.js 7.0 KB

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