CompileController.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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 } = 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 = 'retry'
  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 (
  58. error?.code === 'EPIPE' ||
  59. error instanceof Errors.TooManyCompileRequestsError
  60. ) {
  61. // docker returns EPIPE when shutting down
  62. code = 503 // send 503 Unavailable response
  63. status = 'unavailable'
  64. } else if (error?.terminated) {
  65. status = 'terminated'
  66. } else if (error?.validate) {
  67. status = `validation-${error.validate}`
  68. } else if (error?.timedout) {
  69. status = 'timedout'
  70. logger.debug(
  71. { err: error, projectId: request.project_id },
  72. 'timeout running compile'
  73. )
  74. } else if (error) {
  75. status = 'error'
  76. code = 500
  77. logger.error(
  78. { err: error, projectId: request.project_id },
  79. 'error running compile'
  80. )
  81. } else {
  82. if (
  83. outputFiles.some(
  84. file => file.path === 'output.pdf' && file.size > 0
  85. )
  86. ) {
  87. status = 'success'
  88. lastSuccessfulCompileTimestamp = Date.now()
  89. } else if (request.stopOnFirstError) {
  90. status = 'stopped-on-first-error'
  91. } else {
  92. status = 'failure'
  93. logger.warn(
  94. { projectId: request.project_id, outputFiles },
  95. 'project failed to compile successfully, no output.pdf generated'
  96. )
  97. }
  98. // log an error if any core files are found
  99. if (outputFiles.some(file => file.path === 'core')) {
  100. logger.error(
  101. { projectId: request.project_id, req, outputFiles },
  102. 'core file found in output'
  103. )
  104. }
  105. }
  106. if (error) {
  107. outputFiles = error.outputFiles || []
  108. buildId = error.buildId
  109. }
  110. let clsiCacheShard
  111. if (
  112. status === 'success' &&
  113. request.editorId &&
  114. request.populateClsiCache
  115. ) {
  116. clsiCacheShard = notifyCLSICacheAboutBuild({
  117. projectId: request.project_id,
  118. userId: request.user_id,
  119. buildId: outputFiles[0].build,
  120. editorId: request.editorId,
  121. outputFiles,
  122. compileGroup: request.compileGroup,
  123. stats,
  124. timings,
  125. options: {
  126. compiler: request.compiler,
  127. draft: request.draft,
  128. imageName: request.imageName
  129. ? Path.basename(request.imageName)
  130. : undefined,
  131. rootResourcePath: request.rootResourcePath,
  132. stopOnFirstError: request.stopOnFirstError,
  133. },
  134. })
  135. }
  136. timer.done()
  137. res.status(code || 200).send({
  138. compile: {
  139. status,
  140. error: error?.message || error,
  141. stats,
  142. timings,
  143. buildId,
  144. clsiCacheShard,
  145. outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
  146. outputFiles: outputFiles.map(file => ({
  147. url:
  148. `${Settings.apis.clsi.downloadHost}/project/${request.project_id}` +
  149. (request.user_id != null
  150. ? `/user/${request.user_id}`
  151. : '') +
  152. `/build/${file.build}/output/${file.path}`,
  153. ...file,
  154. })),
  155. },
  156. })
  157. }
  158. )
  159. }
  160. )
  161. })
  162. }
  163. function stopCompile(req, res, next) {
  164. const { project_id: projectId, user_id: userId } = req.params
  165. CompileManager.stopCompile(projectId, userId, function (error) {
  166. if (error) {
  167. return next(error)
  168. }
  169. res.sendStatus(204)
  170. })
  171. }
  172. function clearCache(req, res, next) {
  173. ProjectPersistenceManager.clearProject(
  174. req.params.project_id,
  175. req.params.user_id,
  176. function (error) {
  177. if (error) {
  178. return next(error)
  179. }
  180. // No content
  181. res.sendStatus(204)
  182. }
  183. )
  184. }
  185. function syncFromCode(req, res, next) {
  186. const { file, editorId, buildId } = req.query
  187. const compileFromClsiCache = req.query.compileFromClsiCache === 'true'
  188. const line = parseInt(req.query.line, 10)
  189. const column = parseInt(req.query.column, 10)
  190. const { imageName } = req.query
  191. const projectId = req.params.project_id
  192. const userId = req.params.user_id
  193. CompileManager.syncFromCode(
  194. projectId,
  195. userId,
  196. file,
  197. line,
  198. column,
  199. { imageName, editorId, buildId, compileFromClsiCache },
  200. function (error, pdfPositions, downloadedFromCache) {
  201. if (error) {
  202. return next(error)
  203. }
  204. res.json({
  205. pdf: pdfPositions,
  206. downloadedFromCache,
  207. })
  208. }
  209. )
  210. }
  211. function syncFromPdf(req, res, next) {
  212. const page = parseInt(req.query.page, 10)
  213. const h = parseFloat(req.query.h)
  214. const v = parseFloat(req.query.v)
  215. const { imageName, editorId, buildId } = req.query
  216. const compileFromClsiCache = req.query.compileFromClsiCache === 'true'
  217. const projectId = req.params.project_id
  218. const userId = req.params.user_id
  219. CompileManager.syncFromPdf(
  220. projectId,
  221. userId,
  222. page,
  223. h,
  224. v,
  225. { imageName, editorId, buildId, compileFromClsiCache },
  226. function (error, codePositions, downloadedFromCache) {
  227. if (error) {
  228. return next(error)
  229. }
  230. res.json({
  231. code: codePositions,
  232. downloadedFromCache,
  233. })
  234. }
  235. )
  236. }
  237. function wordcount(req, res, next) {
  238. const file = req.query.file || 'main.tex'
  239. const projectId = req.params.project_id
  240. const userId = req.params.user_id
  241. const { image } = req.query
  242. logger.debug({ image, file, projectId }, 'word count request')
  243. CompileManager.wordcount(
  244. projectId,
  245. userId,
  246. file,
  247. image,
  248. function (error, result) {
  249. if (error) {
  250. return next(error)
  251. }
  252. res.json({
  253. texcount: result,
  254. })
  255. }
  256. )
  257. }
  258. function status(req, res, next) {
  259. res.send('OK')
  260. }
  261. export default {
  262. compile,
  263. stopCompile,
  264. clearCache,
  265. syncFromCode,
  266. syncFromPdf,
  267. wordcount,
  268. status,
  269. timeSinceLastSuccessfulCompile,
  270. }