output-files.js 7.3 KB

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