CompileController.js 8.0 KB

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