CompileController.js 8.2 KB

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