CompileController.js 8.9 KB

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