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