CompileController.js 7.0 KB

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