CompileManager.coffee 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. ResourceWriter = require "./ResourceWriter"
  2. LatexRunner = require "./LatexRunner"
  3. OutputFileFinder = require "./OutputFileFinder"
  4. OutputCacheManager = require "./OutputCacheManager"
  5. Settings = require("settings-sharelatex")
  6. Path = require "path"
  7. logger = require "logger-sharelatex"
  8. Metrics = require "./Metrics"
  9. child_process = require "child_process"
  10. CommandRunner = require(Settings.clsi?.commandRunner or "./CommandRunner")
  11. DraftModeManager = require "./DraftModeManager"
  12. fs = require("fs")
  13. os = require("os")
  14. module.exports = CompileManager =
  15. doCompile: (request, callback = (error, outputFiles) ->) ->
  16. compileDir = Path.join(Settings.path.compilesDir, request.project_id)
  17. timer = new Metrics.Timer("write-to-disk")
  18. logger.log project_id: request.project_id, "starting compile"
  19. ResourceWriter.syncResourcesToDisk request.project_id, request.resources, compileDir, (error) ->
  20. return callback(error) if error?
  21. logger.log project_id: request.project_id, time_taken: Date.now() - timer.start, "written files to disk"
  22. timer.done()
  23. injectDraftModeIfRequired = (callback) ->
  24. if request.draft
  25. DraftModeManager.injectDraftMode Path.join(compileDir, request.rootResourcePath), callback
  26. else
  27. callback()
  28. injectDraftModeIfRequired (error) ->
  29. return callback(error) if error?
  30. timer = new Metrics.Timer("run-compile")
  31. Metrics.inc("compiles")
  32. LatexRunner.runLatex request.project_id, {
  33. directory: compileDir
  34. mainFile: request.rootResourcePath
  35. compiler: request.compiler
  36. timeout: request.timeout
  37. image: request.imageName
  38. }, (error, output, stats) ->
  39. return callback(error) if error?
  40. Metrics.inc("compiles-succeeded")
  41. for metric_key, metric_value of stats or {}
  42. Metrics.count(metric_key, metric_value)
  43. loadavg = os.loadavg?()
  44. Metrics.gauge("load-avg", loadavg[0]) if loadavg?
  45. ts = timer.done()
  46. logger.log {project_id: request.project_id, time_taken: ts, stats:stats, loadavg:loadavg}, "done compile"
  47. if stats?["latex-runs"] > 0
  48. Metrics.timing("run-compile-per-pass", ts / stats["latex-runs"])
  49. OutputFileFinder.findOutputFiles request.resources, compileDir, (error, outputFiles) ->
  50. return callback(error) if error?
  51. OutputCacheManager.saveOutputFiles outputFiles, compileDir, (error, newOutputFiles) ->
  52. callback null, newOutputFiles
  53. clearProject: (project_id, _callback = (error) ->) ->
  54. callback = (error) ->
  55. _callback(error)
  56. _callback = () ->
  57. compileDir = Path.join(Settings.path.compilesDir, project_id)
  58. CompileManager._checkDirectory compileDir, (err, exists) ->
  59. return callback(err) if err?
  60. return callback() if not exists # skip removal if no directory present
  61. proc = child_process.spawn "rm", ["-r", compileDir]
  62. proc.on "error", callback
  63. stderr = ""
  64. proc.stderr.on "data", (chunk) -> stderr += chunk.toString()
  65. proc.on "close", (code) ->
  66. if code == 0
  67. return callback(null)
  68. else
  69. return callback(new Error("rm -r #{compileDir} failed: #{stderr}"))
  70. _checkDirectory: (compileDir, callback = (error, exists) ->) ->
  71. fs.lstat compileDir, (err, stats) ->
  72. if err?.code is 'ENOENT'
  73. return callback(null, false) # directory does not exist
  74. else if err?
  75. logger.err {dir: compileDir, err:err}, "error on stat of project directory for removal"
  76. return callback(err)
  77. else if not stats?.isDirectory()
  78. logger.err {dir: compileDir, stats:stats}, "bad project directory for removal"
  79. return callback new Error("project directory is not directory")
  80. else
  81. callback(null, true) # directory exists
  82. syncFromCode: (project_id, file_name, line, column, callback = (error, pdfPositions) ->) ->
  83. # If LaTeX was run in a virtual environment, the file path that synctex expects
  84. # might not match the file path on the host. The .synctex.gz file however, will be accessed
  85. # wherever it is on the host.
  86. base_dir = Settings.path.synctexBaseDir(project_id)
  87. file_path = base_dir + "/" + file_name
  88. synctex_path = Path.join(Settings.path.compilesDir, project_id, "output.pdf")
  89. CompileManager._runSynctex ["code", synctex_path, file_path, line, column], (error, stdout) ->
  90. return callback(error) if error?
  91. logger.log project_id: project_id, file_name: file_name, line: line, column: column, stdout: stdout, "synctex code output"
  92. callback null, CompileManager._parseSynctexFromCodeOutput(stdout)
  93. syncFromPdf: (project_id, page, h, v, callback = (error, filePositions) ->) ->
  94. base_dir = Settings.path.synctexBaseDir(project_id)
  95. synctex_path = Path.join(Settings.path.compilesDir, project_id, "output.pdf")
  96. CompileManager._runSynctex ["pdf", synctex_path, page, h, v], (error, stdout) ->
  97. return callback(error) if error?
  98. logger.log project_id: project_id, page: page, h: h, v:v, stdout: stdout, "synctex pdf output"
  99. callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)
  100. _runSynctex: (args, callback = (error, stdout) ->) ->
  101. bin_path = Path.resolve(__dirname + "/../../bin/synctex")
  102. seconds = 1000
  103. child_process.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) ->
  104. return callback(error) if error?
  105. callback(null, stdout)
  106. _parseSynctexFromCodeOutput: (output) ->
  107. results = []
  108. for line in output.split("\n")
  109. [node, page, h, v, width, height] = line.split("\t")
  110. if node == "NODE"
  111. results.push {
  112. page: parseInt(page, 10)
  113. h: parseFloat(h)
  114. v: parseFloat(v)
  115. height: parseFloat(height)
  116. width: parseFloat(width)
  117. }
  118. return results
  119. _parseSynctexFromPdfOutput: (output, base_dir) ->
  120. results = []
  121. for line in output.split("\n")
  122. [node, file_path, line, column] = line.split("\t")
  123. if node == "NODE"
  124. file = file_path.slice(base_dir.length + 1)
  125. results.push {
  126. file: file
  127. line: parseInt(line, 10)
  128. column: parseInt(column, 10)
  129. }
  130. return results
  131. wordcount: (project_id, file_name, image, callback = (error, pdfPositions) ->) ->
  132. logger.log project_id:project_id, file_name:file_name, image:image, "running wordcount"
  133. file_path = "$COMPILE_DIR/" + file_name
  134. command = [ "texcount", '-inc', file_path, "-out=" + file_path + ".wc"]
  135. directory = Path.join(Settings.path.compilesDir, project_id)
  136. timeout = 10 * 1000
  137. CommandRunner.run project_id, command, directory, image, timeout, (error) ->
  138. return callback(error) if error?
  139. try
  140. stdout = fs.readFileSync(directory + "/" + file_name + ".wc", "utf-8")
  141. catch err
  142. logger.err err:err, command:command, directory:directory, project_id:project_id, "error reading word count output"
  143. return callback(err)
  144. callback null, CompileManager._parseWordcountFromOutput(stdout)
  145. _parseWordcountFromOutput: (output) ->
  146. results = {
  147. encode: ""
  148. textWords: 0
  149. headWords: 0
  150. outside: 0
  151. headers: 0
  152. elements: 0
  153. mathInline: 0
  154. mathDisplay: 0
  155. }
  156. for line in output.split("\n")
  157. [data, info] = line.split(":")
  158. if data.indexOf("Encoding") > -1
  159. results['encode'] = info.trim()
  160. if data.indexOf("in text") > -1
  161. results['textWords'] = parseInt(info, 10)
  162. if data.indexOf("in head") > -1
  163. results['headWords'] = parseInt(info, 10)
  164. if data.indexOf("outside") > -1
  165. results['outside'] = parseInt(info, 10)
  166. if data.indexOf("of head") > -1
  167. results['headers'] = parseInt(info, 10)
  168. if data.indexOf("Number of floats/tables/figures") > -1
  169. results['elements'] = parseInt(info, 10)
  170. if data.indexOf("Number of math inlines") > -1
  171. results['mathInline'] = parseInt(info, 10)
  172. if data.indexOf("Number of math displayed") > -1
  173. results['mathDisplay'] = parseInt(info, 10)
  174. return results