CompileController.js 7.5 KB

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