CompileController.js 8.7 KB

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