output-files.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import HumanReadableLogs from '../../../ide/human-readable-logs/HumanReadableLogs'
  2. import BibLogParser from '../../../ide/log-parser/bib-log-parser'
  3. import { enablePdfCaching } from './pdf-caching-flags'
  4. import { debugConsole } from '@/utils/debugging'
  5. import { dirname, findEntityByPath } from '@/features/file-tree/util/path'
  6. import '@/utils/readable-stream-async-iterator-polyfill'
  7. // Warnings that may disappear after a second LaTeX pass
  8. const TRANSIENT_WARNING_REGEX = /^(Reference|Citation).+undefined on input line/
  9. const MAX_LOG_SIZE = 1024 * 1024 // 1MB
  10. export function handleOutputFiles(outputFiles, projectId, data) {
  11. const outputFile = outputFiles.get('output.pdf')
  12. if (!outputFile) return null
  13. // build the URL for viewing the PDF in the preview UI
  14. const params = new URLSearchParams({
  15. compileGroup: data.compileGroup,
  16. })
  17. if (data.clsiServerId) {
  18. params.set('clsiserverid', data.clsiServerId)
  19. }
  20. if (enablePdfCaching) {
  21. // Tag traffic that uses the pdf caching logic.
  22. params.set('enable_pdf_caching', 'true')
  23. }
  24. outputFile.pdfUrl = `${buildURL(
  25. outputFile,
  26. data.pdfDownloadDomain
  27. )}?${params}`
  28. // build the URL for downloading the PDF
  29. params.set('popupDownload', 'true') // save PDF download as file
  30. outputFile.pdfDownloadUrl = `/download/project/${projectId}/build/${outputFile.build}/output/output.pdf?${params}`
  31. return outputFile
  32. }
  33. let nextEntryId = 1
  34. function generateEntryKey() {
  35. return 'compile-log-entry-' + nextEntryId++
  36. }
  37. export const handleLogFiles = async (outputFiles, data, signal) => {
  38. const result = {
  39. log: null,
  40. logEntries: {
  41. errors: [],
  42. warnings: [],
  43. typesetting: [],
  44. },
  45. }
  46. function accumulateResults(newEntries, type) {
  47. for (const key in result.logEntries) {
  48. if (newEntries[key]) {
  49. for (const entry of newEntries[key]) {
  50. if (type) {
  51. entry.type = newEntries.type
  52. }
  53. if (entry.file) {
  54. entry.file = normalizeFilePath(entry.file)
  55. }
  56. entry.key = generateEntryKey()
  57. }
  58. result.logEntries[key].push(...newEntries[key])
  59. }
  60. }
  61. }
  62. const logFile = outputFiles.get('output.log')
  63. if (logFile) {
  64. try {
  65. const logFileAbortController = new AbortController()
  66. // abort fetching the log file if the main signal is aborted
  67. signal.addEventListener('abort', () => {
  68. logFileAbortController.abort()
  69. })
  70. const response = await fetch(buildURL(logFile, data.pdfDownloadDomain), {
  71. signal: logFileAbortController.signal,
  72. })
  73. result.log = ''
  74. const reader = response.body.pipeThrough(new TextDecoderStream())
  75. for await (const chunk of reader) {
  76. result.log += chunk
  77. if (result.log.length > MAX_LOG_SIZE) {
  78. logFileAbortController.abort()
  79. }
  80. }
  81. } catch (e) {
  82. debugConsole.warn(e) // ignore failure to fetch the log file, but log a warning
  83. }
  84. try {
  85. let { errors, warnings, typesetting } = HumanReadableLogs.parse(
  86. result.log,
  87. {
  88. ignoreDuplicates: true,
  89. }
  90. )
  91. if (data.status === 'stopped-on-first-error') {
  92. // Hide warnings that could disappear after a second pass
  93. warnings = warnings.filter(warning => !isTransientWarning(warning))
  94. }
  95. accumulateResults({ errors, warnings, typesetting })
  96. } catch (e) {
  97. debugConsole.warn(e) // ignore failure to parse the log file, but log a warning
  98. }
  99. }
  100. const blgFiles = []
  101. for (const [filename, file] of outputFiles) {
  102. if (filename.endsWith('.blg')) {
  103. blgFiles.push(file)
  104. }
  105. }
  106. for (const blgFile of blgFiles) {
  107. try {
  108. const response = await fetch(buildURL(blgFile, data.pdfDownloadDomain), {
  109. signal,
  110. })
  111. const log = await response.text()
  112. try {
  113. const { errors, warnings } = new BibLogParser(log, {
  114. maxErrors: 100,
  115. }).parse()
  116. accumulateResults({ errors, warnings }, 'BibTeX:')
  117. } catch (e) {
  118. // BibLog parsing errors are ignored
  119. }
  120. } catch (e) {
  121. debugConsole.warn(e) // ignore failure to fetch/parse the log file, but log a warning
  122. }
  123. }
  124. result.logEntries.all = [
  125. ...result.logEntries.errors,
  126. ...result.logEntries.warnings,
  127. ...result.logEntries.typesetting,
  128. ]
  129. return result
  130. }
  131. export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) {
  132. const rootDocDirname = dirname(fileTreeData, rootDocId)
  133. const logEntryAnnotations = {}
  134. const seenLine = {}
  135. for (const entry of entries) {
  136. if (entry.file) {
  137. entry.file = normalizeFilePath(entry.file, rootDocDirname)
  138. const entity = findEntityByPath(fileTreeData, entry.file)?.entity
  139. if (entity) {
  140. if (!(entity._id in logEntryAnnotations)) {
  141. logEntryAnnotations[entity._id] = []
  142. }
  143. const annotation = {
  144. id: entry.key,
  145. entryIndex: logEntryAnnotations[entity._id].length, // used for maintaining the order of items on the same line
  146. row: entry.line - 1,
  147. type: entry.level === 'error' ? 'error' : 'warning',
  148. text: entry.message,
  149. source: 'compile', // NOTE: this is used in Ace for filtering the annotations
  150. ruleId: entry.ruleId,
  151. }
  152. // set firstOnLine for the first non-typesetting annotation on a line
  153. if (entry.level !== 'typesetting') {
  154. if (!seenLine[entry.line]) {
  155. annotation.firstOnLine = true
  156. seenLine[entry.line] = true
  157. }
  158. }
  159. logEntryAnnotations[entity._id].push(annotation)
  160. }
  161. }
  162. }
  163. return logEntryAnnotations
  164. }
  165. export const buildRuleCounts = (entries = []) => {
  166. const counts = {}
  167. for (const entry of entries) {
  168. const key = `${entry.level}_${entry.ruleId}`
  169. counts[key] = counts[key] ? counts[key] + 1 : 1
  170. }
  171. return counts
  172. }
  173. export const buildRuleDeltas = (ruleCounts, previousRuleCounts) => {
  174. const counts = {}
  175. // keys that are defined in the current log entries
  176. for (const [key, value] of Object.entries(ruleCounts)) {
  177. const previousValue = previousRuleCounts[key] ?? 0
  178. counts[`delta_${key}`] = value - previousValue
  179. }
  180. // keys that are no longer defined in the current log entries
  181. for (const [key, value] of Object.entries(previousRuleCounts)) {
  182. if (!(key in ruleCounts)) {
  183. counts[key] = 0
  184. counts[`delta_${key}`] = -value
  185. }
  186. }
  187. return counts
  188. }
  189. function buildURL(file, pdfDownloadDomain) {
  190. if (file.build && pdfDownloadDomain) {
  191. // Downloads from the compiles domain must include a build id.
  192. // The build id is used implicitly for access control.
  193. return `${pdfDownloadDomain}${file.url}`
  194. }
  195. // Go through web instead, which uses mongo for checking project access.
  196. return `${window.origin}${file.url}`
  197. }
  198. function normalizeFilePath(path, rootDocDirname) {
  199. path = path.replace(/\/\//g, '/')
  200. path = path.replace(
  201. /^.*\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/,
  202. ''
  203. )
  204. path = path.replace(/^\/compile\//, '')
  205. if (rootDocDirname) {
  206. path = path.replace(/^\.\//, rootDocDirname + '/')
  207. }
  208. return path
  209. }
  210. function isTransientWarning(warning) {
  211. return TRANSIENT_WARNING_REGEX.test(warning.message)
  212. }