CompileController.js 6.9 KB

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