CompileController.js 8.7 KB

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