CompileController.js 7.9 KB

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