CompileController.js 8.5 KB

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