CompileController.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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. let clsiCacheShard
  110. if (
  111. status === 'success' &&
  112. request.editorId &&
  113. request.populateClsiCache
  114. ) {
  115. clsiCacheShard = notifyCLSICacheAboutBuild({
  116. projectId: request.project_id,
  117. userId: request.user_id,
  118. buildId: outputFiles[0].build,
  119. editorId: request.editorId,
  120. outputFiles,
  121. compileGroup: request.compileGroup,
  122. options: {
  123. compiler: request.compiler,
  124. draft: request.draft,
  125. imageName: request.imageName
  126. ? Path.basename(request.imageName)
  127. : undefined,
  128. rootResourcePath: request.rootResourcePath,
  129. stopOnFirstError: request.stopOnFirstError,
  130. },
  131. })
  132. }
  133. timer.done()
  134. res.status(code || 200).send({
  135. compile: {
  136. status,
  137. error: error?.message || error,
  138. stats,
  139. timings,
  140. buildId,
  141. clsiCacheShard,
  142. outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
  143. outputFiles: outputFiles.map(file => ({
  144. url:
  145. `${Settings.apis.clsi.url}/project/${request.project_id}` +
  146. (request.user_id != null
  147. ? `/user/${request.user_id}`
  148. : '') +
  149. `/build/${file.build}/output/${file.path}`,
  150. ...file,
  151. })),
  152. },
  153. })
  154. }
  155. )
  156. }
  157. )
  158. })
  159. }
  160. function stopCompile(req, res, next) {
  161. const { project_id: projectId, user_id: userId } = req.params
  162. CompileManager.stopCompile(projectId, userId, function (error) {
  163. if (error) {
  164. return next(error)
  165. }
  166. res.sendStatus(204)
  167. })
  168. }
  169. function clearCache(req, res, next) {
  170. ProjectPersistenceManager.clearProject(
  171. req.params.project_id,
  172. req.params.user_id,
  173. function (error) {
  174. if (error) {
  175. return next(error)
  176. }
  177. // No content
  178. res.sendStatus(204)
  179. }
  180. )
  181. }
  182. function syncFromCode(req, res, next) {
  183. const { file, editorId, buildId, compileFromClsiCache } = req.query
  184. const line = parseInt(req.query.line, 10)
  185. const column = parseInt(req.query.column, 10)
  186. const { imageName } = req.query
  187. const projectId = req.params.project_id
  188. const userId = req.params.user_id
  189. CompileManager.syncFromCode(
  190. projectId,
  191. userId,
  192. file,
  193. line,
  194. column,
  195. { imageName, editorId, buildId, compileFromClsiCache },
  196. function (error, pdfPositions) {
  197. if (error) {
  198. return next(error)
  199. }
  200. res.json({
  201. pdf: pdfPositions,
  202. })
  203. }
  204. )
  205. }
  206. function syncFromPdf(req, res, next) {
  207. const page = parseInt(req.query.page, 10)
  208. const h = parseFloat(req.query.h)
  209. const v = parseFloat(req.query.v)
  210. const { imageName, editorId, buildId, compileFromClsiCache } = req.query
  211. const projectId = req.params.project_id
  212. const userId = req.params.user_id
  213. CompileManager.syncFromPdf(
  214. projectId,
  215. userId,
  216. page,
  217. h,
  218. v,
  219. { imageName, editorId, buildId, compileFromClsiCache },
  220. function (error, codePositions) {
  221. if (error) {
  222. return next(error)
  223. }
  224. res.json({
  225. code: codePositions,
  226. })
  227. }
  228. )
  229. }
  230. function wordcount(req, res, next) {
  231. const file = req.query.file || 'main.tex'
  232. const projectId = req.params.project_id
  233. const userId = req.params.user_id
  234. const { image } = req.query
  235. logger.debug({ image, file, projectId }, 'word count request')
  236. CompileManager.wordcount(
  237. projectId,
  238. userId,
  239. file,
  240. image,
  241. function (error, result) {
  242. if (error) {
  243. return next(error)
  244. }
  245. res.json({
  246. texcount: result,
  247. })
  248. }
  249. )
  250. }
  251. function status(req, res, next) {
  252. res.send('OK')
  253. }
  254. module.exports = {
  255. compile,
  256. stopCompile,
  257. clearCache,
  258. syncFromCode,
  259. syncFromPdf,
  260. wordcount,
  261. status,
  262. timeSinceLastSuccessfulCompile,
  263. }