CompileController.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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 { 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. }
  97. timer.done()
  98. res.status(code || 200).send({
  99. compile: {
  100. status,
  101. error: error?.message || error,
  102. stats,
  103. timings,
  104. outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
  105. outputFiles: outputFiles.map(file => ({
  106. url:
  107. `${Settings.apis.clsi.url}/project/${request.project_id}` +
  108. (request.user_id != null ? `/user/${request.user_id}` : '') +
  109. (file.build != null ? `/build/${file.build}` : '') +
  110. `/output/${file.path}`,
  111. ...file,
  112. })),
  113. },
  114. })
  115. })
  116. }
  117. )
  118. })
  119. }
  120. function stopCompile(req, res, next) {
  121. const { project_id: projectId, user_id: userId } = req.params
  122. CompileManager.stopCompile(projectId, userId, function (error) {
  123. if (error) {
  124. return next(error)
  125. }
  126. res.sendStatus(204)
  127. })
  128. }
  129. function clearCache(req, res, next) {
  130. ProjectPersistenceManager.clearProject(
  131. req.params.project_id,
  132. req.params.user_id,
  133. function (error) {
  134. if (error) {
  135. return next(error)
  136. }
  137. // No content
  138. res.sendStatus(204)
  139. }
  140. )
  141. }
  142. function syncFromCode(req, res, next) {
  143. const { file } = req.query
  144. const line = parseInt(req.query.line, 10)
  145. const column = parseInt(req.query.column, 10)
  146. const { imageName } = req.query
  147. const projectId = req.params.project_id
  148. const userId = req.params.user_id
  149. if (imageName && !_isImageNameAllowed(imageName)) {
  150. return res.status(400).send('invalid image')
  151. }
  152. CompileManager.syncFromCode(
  153. projectId,
  154. userId,
  155. file,
  156. line,
  157. column,
  158. imageName,
  159. function (error, pdfPositions) {
  160. if (error) {
  161. return next(error)
  162. }
  163. res.json({
  164. pdf: pdfPositions,
  165. })
  166. }
  167. )
  168. }
  169. function syncFromPdf(req, res, next) {
  170. const page = parseInt(req.query.page, 10)
  171. const h = parseFloat(req.query.h)
  172. const v = parseFloat(req.query.v)
  173. const { imageName } = req.query
  174. const projectId = req.params.project_id
  175. const userId = req.params.user_id
  176. if (imageName && !_isImageNameAllowed(imageName)) {
  177. return res.status(400).send('invalid image')
  178. }
  179. CompileManager.syncFromPdf(
  180. projectId,
  181. userId,
  182. page,
  183. h,
  184. v,
  185. imageName,
  186. function (error, codePositions) {
  187. if (error) {
  188. return next(error)
  189. }
  190. res.json({
  191. code: codePositions,
  192. })
  193. }
  194. )
  195. }
  196. function wordcount(req, res, next) {
  197. const file = req.query.file || 'main.tex'
  198. const projectId = req.params.project_id
  199. const userId = req.params.user_id
  200. const { image } = req.query
  201. if (image && !_isImageNameAllowed(image)) {
  202. return res.status(400).send('invalid image')
  203. }
  204. logger.debug({ image, file, projectId }, 'word count request')
  205. CompileManager.wordcount(
  206. projectId,
  207. userId,
  208. file,
  209. image,
  210. function (error, result) {
  211. if (error) {
  212. return next(error)
  213. }
  214. res.json({
  215. texcount: result,
  216. })
  217. }
  218. )
  219. }
  220. function status(req, res, next) {
  221. res.send('OK')
  222. }
  223. function _isImageNameAllowed(imageName) {
  224. const ALLOWED_IMAGES =
  225. Settings.clsi && Settings.clsi.docker && Settings.clsi.docker.allowedImages
  226. return !ALLOWED_IMAGES || ALLOWED_IMAGES.includes(imageName)
  227. }
  228. module.exports = {
  229. compile,
  230. stopCompile,
  231. clearCache,
  232. syncFromCode,
  233. syncFromPdf,
  234. wordcount,
  235. status,
  236. timeSinceLastSuccessfulCompile,
  237. }