CompileController.js 7.6 KB

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