1 /*
2  * SessionCompilePdf.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include "SessionCompilePdf.hpp"
17 
18 #include <set>
19 
20 #include <boost/format.hpp>
21 #include <boost/enable_shared_from_this.hpp>
22 
23 #include <shared_core/FilePath.hpp>
24 #include <core/Exec.hpp>
25 #include <core/Settings.hpp>
26 #include <core/Algorithm.hpp>
27 #include <core/FileSerializer.hpp>
28 #include <core/system/ShellUtils.hpp>
29 
30 #include <core/tex/TexLogParser.hpp>
31 #include <core/tex/TexMagicComment.hpp>
32 
33 #include <r/RSexp.hpp>
34 #include <r/RExec.hpp>
35 #include <r/RRoutines.hpp>
36 
37 #include <session/SessionModuleContext.hpp>
38 #include <session/projects/SessionProjects.hpp>
39 #include <session/prefs/UserPrefs.hpp>
40 
41 #include "SessionPdfLatex.hpp"
42 #include "SessionRnwWeave.hpp"
43 #include "SessionRnwConcordance.hpp"
44 #include "SessionSynctex.hpp"
45 #include "SessionCompilePdfSupervisor.hpp"
46 #include "SessionViewPdf.hpp"
47 #include "SessionTexUtils.hpp"
48 
49 using namespace rstudio::core;
50 
51 namespace rstudio {
52 namespace session {
53 namespace modules {
54 namespace tex {
55 namespace compile_pdf {
56 
57 namespace {
58 
59 // track compile pdf state so we can send it to the client on client init
60 class CompilePdfState : boost::noncopyable
61 {
62 public:
CompilePdfState()63    CompilePdfState()
64       : tabVisible_(false), running_(false)
65    {
66    }
67 
~CompilePdfState()68    virtual ~CompilePdfState() {}
69    // COPYING: noncoypable
70 
readFromJson(const json::Object & asJson)71    Error readFromJson(const json::Object& asJson)
72    {
73       clear();
74       return json::readObject(asJson,
75                               "tab_visible", tabVisible_,
76                               "running", running_,
77                               "target_file", targetFile_,
78                               "output", output_,
79                               "errors", errors_);
80    }
81 
82 
onStarted(const std::string & targetFile)83    void onStarted(const std::string& targetFile)
84    {
85       clear();
86       running_ = true;
87       tabVisible_ = true;
88       targetFile_ = targetFile;
89    }
90 
addOutput(const std::string & output)91    void addOutput(const std::string& output)
92    {
93       output_ += output;
94    }
95 
setErrors(const json::Array & errors)96    void setErrors(const json::Array& errors)
97    {
98       errors_ = errors;
99    }
100 
onCompleted()101    void onCompleted()
102    {
103       running_ = false;
104    }
105 
clear()106    void clear()
107    {
108       tabVisible_ = false;
109       running_ = false;
110       targetFile_.clear();
111       output_.clear();
112       errors_.clear();
113    }
114 
asJson() const115    json::Object asJson() const
116    {
117       json::Object obj;
118       obj["tab_visible"] = tabVisible_;
119       obj["running"] = running_;
120       obj["target_file"] = targetFile_;
121       obj["output"] = output_;
122       obj["errors"] = errors_;
123       return obj;
124    }
125 
126 private:
127    bool tabVisible_;
128    bool running_;
129    std::string targetFile_;
130    std::string output_;
131    json::Array errors_;
132 };
133 
134 CompilePdfState s_compilePdfState;
135 
onSuspend(Settings * pSettings)136 void onSuspend(Settings* pSettings)
137 {
138    pSettings->set("compile_pdf_state", s_compilePdfState.asJson().write());
139 }
140 
141 
onResume(const Settings & settings)142 void onResume(const Settings& settings)
143 {
144    std::string state = settings.get("compile_pdf_state");
145    if (!state.empty())
146    {
147       json::Value stateJson;
148       if (stateJson.parse(state))
149       {
150          LOG_WARNING_MESSAGE("invalid compile pdf state json");
151          return;
152       }
153 
154       Error error = s_compilePdfState.readFromJson(stateJson.getObject());
155       if (error)
156          LOG_ERROR(error);
157    }
158 }
159 
ancillaryFilePath(const FilePath & texFilePath,const std::string & ext)160 FilePath ancillaryFilePath(const FilePath& texFilePath, const std::string& ext)
161 {
162    return texFilePath.getParent().completeChildPath(texFilePath.getStem() + ext);
163 }
164 
isSynctexAvailable(const FilePath & texFilePath)165 bool isSynctexAvailable(const FilePath& texFilePath)
166 {
167    return ancillaryFilePath(texFilePath, ".synctex.gz").exists() ||
168           ancillaryFilePath(texFilePath, ".synctex").exists();
169 }
170 
enqueOutputEvent(const std::string & output)171 void enqueOutputEvent(const std::string& output)
172 {
173    s_compilePdfState.addOutput(output);
174 
175    using namespace module_context;
176    CompileOutput compileOutput(kCompileOutputNormal, output);
177 
178    ClientEvent event(client_events::kCompilePdfOutputEvent,
179                      compileOutputAsJson(compileOutput));
180 
181    module_context::enqueClientEvent(event);
182 }
183 
enqueStartedEvent(const FilePath & texFilePath)184 void enqueStartedEvent(const FilePath& texFilePath)
185 {
186    std::string targetFile =
187                 module_context::createAliasedPath(texFilePath);
188 
189    s_compilePdfState.onStarted(targetFile);
190 
191    json::Object dataJson;
192    dataJson["target_file"] = targetFile;
193    FilePath pdfPath = ancillaryFilePath(texFilePath, ".pdf");
194    dataJson["pdf_path"] = module_context::createAliasedPath(pdfPath);
195 
196    ClientEvent event(client_events::kCompilePdfStartedEvent,
197                      dataJson);
198 
199    module_context::enqueClientEvent(event);
200 }
201 
enqueCompletedEvent(bool succeeded,const json::Object & sourceLocation,const FilePath & texFilePath)202 void enqueCompletedEvent(bool succeeded,
203                          const json::Object& sourceLocation,
204                          const FilePath& texFilePath)
205 {
206    s_compilePdfState.onCompleted();
207 
208    json::Object dataJson;
209    dataJson["succeeded"] = succeeded;
210    dataJson["target_file"] =
211                   module_context::createAliasedPath(texFilePath);
212 
213    FilePath pdfPath = ancillaryFilePath(texFilePath, ".pdf");
214    dataJson["pdf_path"] = module_context::createAliasedPath(pdfPath);
215    dataJson["view_pdf_url"] = tex::view_pdf::createViewPdfUrl(pdfPath);
216    bool synctexAvailable = isSynctexAvailable(texFilePath);
217    dataJson["synctex_available"] = synctexAvailable;
218    if (synctexAvailable)
219    {
220       json::Value pdfLocation;
221       Error error = modules::tex::synctex::forwardSearch(texFilePath,
222                                                          sourceLocation,
223                                                          &pdfLocation);
224       if (error)
225          LOG_ERROR(error);
226       dataJson["pdf_location"] = pdfLocation;
227    }
228 
229    ClientEvent event(client_events::kCompilePdfCompletedEvent,
230                      dataJson);
231    module_context::enqueClientEvent(event);
232 }
233 
enqueCompletedWithFailureEvent(const FilePath & texFilePath,json::Object & sourceLocation)234 void enqueCompletedWithFailureEvent(const FilePath& texFilePath,
235                                     json::Object& sourceLocation)
236 {
237    enqueCompletedEvent(false, sourceLocation, texFilePath);
238 }
239 
enqueCompletedWithSuccessEvent(const FilePath & texFilePath,const json::Object & sourceLocation)240 void enqueCompletedWithSuccessEvent(const FilePath& texFilePath,
241                                     const json::Object& sourceLocation)
242 {
243    enqueCompletedEvent(true, sourceLocation, texFilePath);
244 }
245 
enqueErrorsEvent(const json::Array & logEntriesJson)246 void enqueErrorsEvent(const json::Array& logEntriesJson)
247 {
248    s_compilePdfState.setErrors(logEntriesJson);
249 
250    ClientEvent event(client_events::kCompilePdfErrorsEvent, logEntriesJson);
251    module_context::enqueClientEvent(event);
252 }
253 
254 // NOTE: sync changes with SessionModuleContext.cpp sourceMarkerJson
logEntryJson(const core::tex::LogEntry & logEntry)255 json::Object logEntryJson(const core::tex::LogEntry& logEntry)
256 {
257    json::Object obj;
258    obj["type"] = static_cast<int>(logEntry.type());
259    obj["path"] = module_context::createAliasedPath(logEntry.filePath());
260    obj["line"] = logEntry.line();
261    obj["column"] = 1;
262    obj["message"] = core::html_utils::HTML(logEntry.message()).text();
263    obj["log_path"] = module_context::createAliasedPath(logEntry.logFilePath());
264    obj["log_line"] = logEntry.logLine();
265    obj["show_error_list"] = true;
266    return obj;
267 }
268 
showLogEntries(const core::tex::LogEntries & logEntries,const rnw_concordance::Concordances & rnwConcordances=rnw_concordance::Concordances ())269 void showLogEntries(const core::tex::LogEntries& logEntries,
270                     const rnw_concordance::Concordances& rnwConcordances =
271                                              rnw_concordance::Concordances())
272 {
273    json::Array logEntriesJson;
274    for (const core::tex::LogEntry& logEntry : logEntries)
275    {
276       using namespace tex::rnw_concordance;
277       core::tex::LogEntry rnwEntry = rnwConcordances.fixup(logEntry);
278       logEntriesJson.push_back(logEntryJson(rnwEntry));
279    }
280 
281    enqueErrorsEvent(logEntriesJson);
282 }
283 
writeLogEntriesOutput(const core::tex::LogEntries & logEntries)284 void writeLogEntriesOutput(const core::tex::LogEntries& logEntries)
285 {
286    if (logEntries.empty())
287       return;
288 
289    std::string output = "\n";
290    for (const core::tex::LogEntry& logEntry : logEntries)
291    {
292       switch(logEntry.type())
293       {
294          case core::tex::LogEntry::Error:
295             output += "Error: ";
296             break;
297          case core::tex::LogEntry::Warning:
298             output += "Warning: ";
299             break;
300          case core::tex::LogEntry::Box:
301             output += "Bad Box: ";
302             break;
303       }
304 
305       output += logEntry.filePath().getFilename();
306       int line = logEntry.line();
307       if (line >= 0)
308          output += ":" + safe_convert::numberToString(line);
309 
310       output += ": " + logEntry.message() + "\n";
311    }
312    output += "\n";
313 
314    enqueOutputEvent(output);
315 
316 }
317 
318 
includeLogEntry(const core::tex::LogEntry & logEntry)319 bool includeLogEntry(const core::tex::LogEntry& logEntry)
320 {
321    return true;
322 }
323 
324 // filter out log entries which we view as superflous or distracting
filterLatexLog(const core::tex::LogEntries & logEntries,core::tex::LogEntries * pFilteredLogEntries)325 void filterLatexLog(const core::tex::LogEntries& logEntries,
326                     core::tex::LogEntries* pFilteredLogEntries)
327 {
328    core::algorithm::copy_if(logEntries.begin(),
329                             logEntries.end(),
330                             std::back_inserter(*pFilteredLogEntries),
331                             includeLogEntry);
332 }
333 
isLogEntryFromTargetFile(const core::tex::LogEntry & logEntry,const FilePath & texPath)334 bool isLogEntryFromTargetFile(const core::tex::LogEntry& logEntry,
335                               const FilePath& texPath)
336 {
337    return logEntry.filePath() == texPath;
338 }
339 
getLogEntries(const FilePath & texPath,core::tex::LogEntries * pLogEntries)340 void getLogEntries(const FilePath& texPath,
341                    core::tex::LogEntries* pLogEntries)
342 {
343    // latex log file
344    FilePath logPath = ancillaryFilePath(texPath, ".log");
345    if (logPath.exists())
346    {
347       core::tex::LogEntries logEntries;
348       Error error = core::tex::parseLatexLog(logPath, &logEntries);
349       if (error)
350          LOG_ERROR(error);
351 
352       filterLatexLog(logEntries, pLogEntries);
353 
354       // re-arrange so that issues in the target file always end up at the top
355       // of the error display
356       std::partition(pLogEntries->begin(),
357                      pLogEntries->end(),
358                      boost::bind(isLogEntryFromTargetFile, _1, texPath));
359    }
360 
361    // bibtex log file
362    core::tex::LogEntries bibtexLogEntries;
363    logPath = ancillaryFilePath(texPath, ".blg");
364    if (logPath.exists())
365    {
366       Error error = core::tex::parseBibtexLog(logPath, &bibtexLogEntries);
367       if (error)
368          LOG_ERROR(error);
369    }
370 
371    // concatenate them together
372    std::copy(bibtexLogEntries.begin(),
373              bibtexLogEntries.end(),
374              std::back_inserter(*pLogEntries));
375 }
376 
removeExistingAncillary(const FilePath & texFilePath,const std::string & extension)377 void removeExistingAncillary(const FilePath& texFilePath,
378                              const std::string& extension)
379 {
380    Error error = ancillaryFilePath(texFilePath, extension).removeIfExists();
381    if (error)
382       LOG_ERROR(error);
383 }
384 
removeExistingLatexAncillaryFiles(const FilePath & texFilePath)385 void removeExistingLatexAncillaryFiles(const FilePath& texFilePath)
386 {
387    removeExistingAncillary(texFilePath, ".log");
388    removeExistingAncillary(texFilePath, ".blg");
389    removeExistingAncillary(texFilePath, ".synctex");
390    removeExistingAncillary(texFilePath, ".synctex.gz");
391  }
392 
buildIssuesMessage(const core::tex::LogEntries & logEntries)393 std::string buildIssuesMessage(const core::tex::LogEntries& logEntries)
394 {
395    if (logEntries.empty())
396       return std::string();
397 
398    // count error types
399    int errors = 0, warnings = 0, badBoxes = 0;
400    for (const core::tex::LogEntry& logEntry : logEntries)
401    {
402       if (logEntry.type() == core::tex::LogEntry::Error)
403          errors++;
404       else if (logEntry.type() == core::tex::LogEntry::Warning)
405          warnings++;
406       else if (logEntry.type() == core::tex::LogEntry::Box)
407          badBoxes++;
408    }
409 
410    std::string issues;
411    boost::format fmt("%1% %2%");
412    if (errors > 0)
413    {
414       issues += boost::str(fmt % errors % "error");
415       if (errors > 1)
416          issues += "s";
417    }
418    if (warnings > 0)
419    {
420       if (!issues.empty())
421          issues += ", ";
422       issues += boost::str(fmt % warnings % "warning");
423       if (warnings > 1)
424          issues += "s";
425    }
426    if (badBoxes > 0)
427    {
428       if (!issues.empty())
429          issues += ", ";
430       issues += boost::str(fmt % badBoxes % "bad");
431       if (badBoxes > 1)
432          issues += "boxes";
433       else
434          issues += "box";
435    }
436 
437    if (!issues.empty())
438       return "Issues: " + issues;
439    else
440       return std::string();
441 }
442 
443 class AuxillaryFileCleanupContext : boost::noncopyable
444 {
445 public:
AuxillaryFileCleanupContext()446    AuxillaryFileCleanupContext()
447       : cleanLog_(true)
448    {
449    }
450 
~AuxillaryFileCleanupContext()451    virtual ~AuxillaryFileCleanupContext()
452    {
453       try
454       {
455          cleanup();
456       }
457       catch(...)
458       {
459       }
460    }
461 
init(const FilePath & targetFilePath)462    void init(const FilePath& targetFilePath)
463    {
464       basePath_ = targetFilePath.getParent().completeChildPath(
465          targetFilePath.getStem()).getAbsolutePath();
466    }
467 
preserveLog()468    void preserveLog()
469    {
470       cleanLog_ = false;
471    }
472 
preserveLogReferencedFiles(const core::tex::LogEntries & logEntries)473    void preserveLogReferencedFiles(
474                const core::tex::LogEntries& logEntries)
475    {
476       for (const core::tex::LogEntry& logEntry : logEntries)
477       {
478          logRefFiles_.insert(logEntry.filePath());
479       }
480    }
481 
cleanup()482    void cleanup()
483    {
484       if (!basePath_.empty())
485       {
486          // remove known auxillary files
487          remove(".out");
488          remove(".aux");
489 
490          // only clean bbl if .bib exists
491          if (exists(".bib"))
492             remove(".bbl");
493 
494          // clean anciallary logs if requested (never clean latex log)
495          if (cleanLog_)
496          {
497             remove(".blg");
498          }
499 
500          // reset base path so we only do this once
501          basePath_.clear();
502       }
503    }
504 
505 private:
exists(const std::string & extension)506    bool exists(const std::string& extension)
507    {
508       return FilePath(basePath_ + extension).exists();
509    }
510 
511    // remove the specified file (but don't if it's referenced
512    // from the log)
remove(const std::string & extension)513    void remove(const std::string& extension)
514    {
515       FilePath filePath(basePath_ + extension);
516       if (logRefFiles_.find(filePath) == logRefFiles_.end())
517       {
518          Error error = filePath.removeIfExists();
519          if (error)
520             LOG_ERROR(error);
521       }
522    }
523 
524 private:
525    std::string basePath_;
526    bool cleanLog_;
527    std::set<FilePath> logRefFiles_;
528 };
529 
530 // implement pdf compilation within a class so we can maintain state
531 // accross the various async callbacks the compile is composed of
532 class AsyncPdfCompiler : boost::noncopyable,
533                     public boost::enable_shared_from_this<AsyncPdfCompiler>
534 {
535 public:
start(const FilePath & targetFilePath,const std::string & encoding,const json::Object & sourceLocation,const boost::function<void ()> & onCompleted)536    static void start(const FilePath& targetFilePath,
537                      const std::string& encoding,
538                      const json::Object& sourceLocation,
539                      const boost::function<void()>& onCompleted)
540    {
541       boost::shared_ptr<AsyncPdfCompiler> pCompiler(
542             new AsyncPdfCompiler(targetFilePath,
543                                  encoding,
544                                  sourceLocation,
545                                  onCompleted));
546 
547       pCompiler->start();
548    }
549 
~AsyncPdfCompiler()550    virtual ~AsyncPdfCompiler() {}
551 
552 private:
AsyncPdfCompiler(const FilePath & targetFilePath,const std::string & encoding,const json::Object & sourceLocation,const boost::function<void ()> & onCompleted)553    AsyncPdfCompiler(const FilePath& targetFilePath,
554                     const std::string& encoding,
555                     const json::Object& sourceLocation,
556                     const boost::function<void()>& onCompleted)
557       : targetFilePath_(targetFilePath),
558         encoding_(encoding),
559         sourceLocation_(sourceLocation),
560         onCompleted_(onCompleted)
561    {
562       if (targetFilePath_.exists())
563       {
564          Error error = core::system::realPath(targetFilePath_, &targetFilePath_);
565          if (error)
566             LOG_ERROR(error);
567       }
568    }
569 
start()570    void start()
571    {
572       // enque started event
573       enqueStartedEvent(targetFilePath_);
574 
575       // terminate if the file doesn't exist
576       if (!targetFilePath_.exists())
577       {
578          terminateWithError("Target document not found: '" +
579                                targetFilePath_.getAbsolutePath() + "'");
580          return;
581       }
582 
583       // ensure no spaces in path
584       std::string filename = targetFilePath_.getFilename();
585       if (filename.find(' ') != std::string::npos)
586       {
587          terminateWithError("Invalid filename: '" + filename +
588                      "' (TeX does not understand paths with spaces)");
589          return;
590       }
591 
592       // parse magic comments
593       Error error = core::tex::parseMagicComments(targetFilePath_,
594                                                   &magicComments_);
595       if (error)
596          LOG_ERROR(error);
597 
598       // add tinytex to the PATH if appropriate
599       module_context::addTinytexToPathIfNecessary();
600 
601       // determine tex program path
602       std::string userErrMsg;
603       if (!pdflatex::latexProgramForFile(magicComments_,
604                                          &texProgramPath_,
605                                          &userErrMsg))
606       {
607          terminateWithError(userErrMsg);
608          return;
609       }
610 
611       // see if we need to weave
612       std::string ext = targetFilePath_.getExtensionLowerCase();
613       bool isRnw = ext == ".rnw" || ext == ".snw" || ext == ".nw" || ext == ".rtex";
614       if (isRnw)
615       {
616          // remove existing ancillary files + concordance
617          removeExistingLatexAncillaryFiles(targetFilePath_);
618          removeExistingAncillary(targetFilePath_, "-concordance.tex");
619 
620          // attempt to weave the rnw
621          rnw_weave::runWeave(targetFilePath_,
622                              encoding_,
623                              magicComments_,
624                              enqueOutputEvent,
625                              boost::bind(
626                               &AsyncPdfCompiler::onWeaveCompleted,
627                                  AsyncPdfCompiler::shared_from_this(), _1));
628       }
629       else if (prefs::userPrefs().useTinytex())
630       {
631          runTinytex(targetFilePath_);
632       }
633       else
634       {
635          runLatexCompiler(false);
636       }
637 
638    }
639 
640 private:
641 
onWeaveCompleted(const rnw_weave::Result & result)642    void onWeaveCompleted(const rnw_weave::Result& result)
643    {
644       if (result.succeeded)
645       {
646          if (prefs::userPrefs().useTinytex())
647          {
648             // compute tex file path
649             std::string texFileName = targetFilePath_.getStem() + ".tex";
650             FilePath texFilePath = targetFilePath_.getParent().completePath(texFileName);
651             runTinytex(texFilePath);
652          }
653          else
654          {
655             runLatexCompiler(true, result.concordances);
656          }
657       }
658       else if (!result.errorLogEntries.empty())
659          terminateWithErrorLogEntries(result.errorLogEntries);
660       else
661          terminateWithError(result.errorMessage);
662    }
663 
onTinytexOutput(const std::string & output)664    void onTinytexOutput(const std::string& output)
665    {
666       enqueOutputEvent(output);
667    }
668 
onTinytexCompileCompleted(int status,const std::string & output)669    void onTinytexCompileCompleted(int status, const std::string& output)
670    {
671       onLatexCompileCompleted(status, targetFilePath_);
672    }
673 
runTinytex(const FilePath & latexFilePath)674    void runTinytex(const FilePath& latexFilePath)
675    {
676       Error error;
677 
678       // build arguments
679       using Argument = std::pair<std::string, std::string>;
680       std::vector<Argument> latexmkArgs;
681 
682       std::string file = latexFilePath.getAbsolutePath();
683       latexmkArgs.push_back({std::string(), shell_utils::escape(file)});
684 
685       std::string engine = string_utils::toLower(prefs::userPrefs().defaultLatexProgram());
686       latexmkArgs.push_back({"engine", shell_utils::escape(engine)});
687 
688       bool clean = prefs::userPrefs().cleanTexi2dviOutput();
689       latexmkArgs.push_back({"clean", clean ? "TRUE" : "FALSE"});
690 
691       if (prefs::userPrefs().latexShellEscape())
692       {
693          latexmkArgs.push_back({"engine_args", shell_utils::escape("-shell-escape")});
694       }
695 
696       auto collapse = [](const Argument& argument)
697       {
698          return argument.first.empty()
699                ? argument.second
700                : argument.first + " = " + argument.second;
701       };
702 
703       std::string arguments = core::algorithm::join(
704                latexmkArgs.begin(),
705                latexmkArgs.end(),
706                ", ",
707                collapse);
708 
709       std::string code =
710             "cat('Compiling document with tinytex ... ');"
711             "invisible(tinytex::latexmk(" + arguments + "))";
712 
713       codePath_ = module_context::tempFile("tinytex-runner-", ".R");
714       error = writeStringToFile(codePath_, code);
715       if (error)
716       {
717          terminateWithError(error.getSummary());
718          return;
719       }
720 
721       FilePath rScriptPath;
722       error = module_context::rScriptPath(&rScriptPath);
723       if (error)
724       {
725          terminateWithError(error.getSummary());
726          return;
727       }
728 
729       std::vector<std::string> args;
730       args.push_back("-s");
731       args.push_back("-f");
732       args.push_back(string_utils::utf8ToSystem(codePath_.getAbsolutePath()));
733 
734       error = compile_pdf_supervisor::runProgram(
735                rScriptPath,
736                args,
737                tex::utils::rTexInputsEnvVars(),
738                latexFilePath.getParent(),
739                boost::bind(
740                   &AsyncPdfCompiler::onTinytexOutput,
741                   AsyncPdfCompiler::shared_from_this(),
742                   _1),
743                boost::bind(
744                   &AsyncPdfCompiler::onTinytexCompileCompleted,
745                   AsyncPdfCompiler::shared_from_this(),
746                   _1,
747                   _2));
748 
749       if (error)
750          terminateWithError("Unable to compile pdf: " + error.getSummary());
751 
752    }
753 
runLatexCompiler(bool targetWeaved,const rnw_concordance::Concordances & concordances=rnw_concordance::Concordances ())754    void runLatexCompiler(bool targetWeaved,
755                          const rnw_concordance::Concordances& concordances =
756                                             rnw_concordance::Concordances())
757    {
758       // configure pdflatex options
759       pdflatex::PdfLatexOptions options;
760       options.fileLineError = false;
761       options.syncTex = !isTargetRnw() || !concordances.empty();
762       options.shellEscape = prefs::userPrefs().latexShellEscape();
763 
764       // get back-end version info
765       core::system::ProcessResult result;
766       Error error = core::system::runProgram(
767                   string_utils::utf8ToSystem(texProgramPath_.getAbsolutePath()),
768                   core::shell_utils::ShellArgs() << "--version",
769                   "",
770                   core::system::ProcessOptions(),
771                   &result);
772       if (error)
773          LOG_ERROR(error);
774       else if (result.exitStatus != EXIT_SUCCESS)
775          LOG_ERROR_MESSAGE("Error probing for latex version: "+ result.stdErr);
776       else
777          options.versionInfo = result.stdOut;
778 
779       // compute tex file path
780       FilePath texFilePath;
781       if (targetWeaved)
782       {
783          texFilePath = targetFilePath_.getParent().completePath(
784                                           targetFilePath_.getStem() + ".tex");
785       }
786       else
787       {
788          texFilePath = targetFilePath_;
789       }
790 
791       // remove log files if they exist (avoids confusion created by parsing
792       // old log files for errors)
793       removeExistingLatexAncillaryFiles(texFilePath);
794 
795       // setup cleanup context if clean was specified
796       if (prefs::userPrefs().cleanTexi2dviOutput())
797          auxillaryFileCleanupContext_.init(texFilePath);
798 
799       // run latex compile
800 
801       // this is our "simulated" texi2dvi -- this was originally
802       // coded as a sequence of sync calls to pdflatex, bibtex, and
803       // makeindex. re-coding it as async is going to be a bit
804       // involved so considering that this is not the default
805       // codepath we'll leave it sync for now (and then just call
806       // the (typically) async callback function onLatexCompileCompleted
807       // directly after the function returns
808 
809       enqueOutputEvent("Running " + texProgramPath_.getFilename() +
810                        " on " + texFilePath.getFilename() + "...");
811 
812       error = tex::pdflatex::texToPdf(texProgramPath_,
813                                       texFilePath,
814                                       options,
815                                       &result);
816 
817       if (error)
818       {
819          terminateWithError("Unable to compile pdf: " + error.getSummary());
820       }
821       else
822       {
823          onLatexCompileCompleted(result.exitStatus,
824                                  texFilePath,
825                                  concordances);
826       }
827 
828    }
829 
onLatexCompileCompleted(int exitStatus,const FilePath & texFilePath,const rnw_concordance::Concordances & concords=rnw_concordance::Concordances ())830    void onLatexCompileCompleted(int exitStatus,
831                                 const FilePath& texFilePath,
832                                 const rnw_concordance::Concordances& concords = rnw_concordance::Concordances())
833    {
834       // remove code file if we had one
835       codePath_.removeIfExists();
836 
837       // collect errors from the log
838       core::tex::LogEntries logEntries;
839       getLogEntries(texFilePath, &logEntries);
840 
841       // determine whether they will be shown in the list
842       // list or within the console
843       bool showIssuesList = !isTargetRnw() || !concords.empty();
844 
845       // notify the cleanp context of log entries (so it can
846       // preserve any referenced files)
847       auxillaryFileCleanupContext_.preserveLogReferencedFiles(
848                                                       logEntries);
849 
850       // show log entries and build issues message
851       std::string issuesMsg;
852       if (showIssuesList && !logEntries.empty())
853       {
854          showLogEntries(logEntries, concords);
855          issuesMsg = buildIssuesMessage(logEntries);
856       }
857 
858       if (exitStatus == EXIT_SUCCESS)
859       {
860          FilePath pdfPath = ancillaryFilePath(texFilePath, ".pdf");
861          std::string pdfFile = module_context::createAliasedPath(
862                                                           pdfPath);
863          std::string completed = "completed\n\nCreated PDF: " + pdfFile + "\n";
864          if (!issuesMsg.empty())
865             completed += "\n" + issuesMsg;
866          enqueOutputEvent(completed);
867 
868          // show issues in console if necessary
869          if (!showIssuesList)
870              writeLogEntriesOutput(logEntries);
871 
872          if (onCompleted_)
873             onCompleted_();
874 
875          enqueCompletedWithSuccessEvent(targetFilePath_, sourceLocation_);
876       }
877       else
878       {
879          std::string failedMsg = "failed\n";
880          if (!issuesMsg.empty())
881             failedMsg += "\n" + issuesMsg;
882          enqueOutputEvent(failedMsg);
883 
884          // don't remove the log
885          auxillaryFileCleanupContext_.preserveLog();
886 
887          // if there were no error found in the log file then just
888          // print the error and exit code
889          if (logEntries.empty())
890          {
891             boost::format fmt("Error running %1% (exit code %2%)");
892             std::string msg(boost::str(fmt % texProgramPath_.getAbsolutePath()
893                                            % exitStatus));
894             enqueOutputEvent(msg + "\n");
895          }
896 
897          // show issues in console if necessary
898          if (!showIssuesList)
899             writeLogEntriesOutput(logEntries);
900 
901          enqueCompletedWithFailureEvent(targetFilePath_, sourceLocation_);
902       }
903    }
904 
terminateWithError(const std::string & message)905    void terminateWithError(const std::string& message)
906    {
907       enqueOutputEvent(message + "\n");
908       enqueCompletedWithFailureEvent(targetFilePath_, sourceLocation_);
909    }
910 
terminateWithErrorLogEntries(const core::tex::LogEntries & logEntries)911    void terminateWithErrorLogEntries(const core::tex::LogEntries& logEntries)
912    {
913       showLogEntries(logEntries);
914       enqueCompletedWithFailureEvent(targetFilePath_, sourceLocation_);
915    }
916 
isTargetRnw() const917    bool isTargetRnw() const
918    {
919       return targetFilePath_.getExtensionLowerCase() == ".rnw" ||
920              targetFilePath_.getExtensionLowerCase() == ".rtex";
921    }
922 
923 private:
924    FilePath targetFilePath_;
925    FilePath codePath_;
926    std::string encoding_;
927    json::Object sourceLocation_;
928    const boost::function<void()> onCompleted_;
929    core::tex::TexMagicComments magicComments_;
930    FilePath texProgramPath_;
931    AuxillaryFileCleanupContext auxillaryFileCleanupContext_;
932 };
933 
934 
935 } // anonymous namespace
936 
937 
startCompile(const core::FilePath & targetFilePath,const std::string & encoding,const json::Object & sourceLocation,const boost::function<void ()> & onCompleted)938 bool startCompile(const core::FilePath& targetFilePath,
939                   const std::string& encoding,
940                   const json::Object& sourceLocation,
941                   const boost::function<void()>& onCompleted)
942 {
943    if (!compile_pdf_supervisor::hasRunningChildren())
944    {
945       AsyncPdfCompiler::start(targetFilePath,
946                               encoding,
947                               sourceLocation,
948                               onCompleted);
949       return true;
950    }
951    else
952    {
953       return false;
954    }
955 }
956 
compileIsRunning()957 bool compileIsRunning()
958 {
959    return compile_pdf_supervisor::hasRunningChildren();
960 }
961 
terminateCompile()962 bool terminateCompile()
963 {
964    Error error = compile_pdf_supervisor::terminateAll(
965                                              boost::posix_time::seconds(1));
966    if (error)
967    {
968       LOG_ERROR(error);
969       return false;
970    }
971    else
972    {
973       enqueOutputEvent("\n[Compile PDF Stopped]\n");
974       return true;
975    }
976 }
977 
notifyTabClosed()978 void notifyTabClosed()
979 {
980    s_compilePdfState.clear();
981 }
982 
currentStateAsJson()983 json::Object currentStateAsJson()
984 {
985    return s_compilePdfState.asJson();
986 }
987 
initialize()988 Error initialize()
989 {
990    // register suspend handler
991    using namespace module_context;
992    addSuspendHandler(SuspendHandler(boost::bind(onSuspend, _2), onResume));
993 
994    return Success();
995 }
996 
997 } // namespace compile_pdf
998 } // namespace tex
999 } // namespace modules
1000 } // namespace session
1001 } // namespace rstudio
1002 
1003