1 /*
2  * SessionSourceCpp.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 "SessionSourceCpp.hpp"
17 
18 #include <boost/algorithm/string/trim.hpp>
19 #include <boost/algorithm/string/predicate.hpp>
20 #include <boost/algorithm/string/join.hpp>
21 
22 #include <core/BoostSignals.hpp>
23 #include <shared_core/Error.hpp>
24 #include <shared_core/FilePath.hpp>
25 #include <core/StringUtils.hpp>
26 
27 #include <r/RSexp.hpp>
28 #include <r/RRoutines.hpp>
29 
30 #include <session/SessionModuleContext.hpp>
31 
32 #include "SessionBuildErrors.hpp"
33 
34 using namespace rstudio::core;
35 
36 namespace rstudio {
37 namespace session {
38 namespace modules {
39 namespace build {
40 namespace source_cpp {
41 
42 namespace {
43 
44 struct SourceCppState
45 {
emptyrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState46    bool empty() const { return errors.isEmpty() && outputs.isEmpty(); }
47 
clearrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState48    void clear()
49    {
50       targetFile.clear();
51       errors.clear();
52       outputs.clear();
53    }
54 
addOutputrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState55    void addOutput(int type, const std::string& output)
56    {
57       using namespace module_context;
58       outputs.push_back(compileOutputAsJson(CompileOutput(type,output)));
59    }
60 
asJsonrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState61    json::Value asJson() const
62    {
63       json::Object stateJson;
64       stateJson["target_file"] = targetFile;
65       stateJson["outputs"] = outputs;
66       stateJson["errors"] = errors;
67       return std::move(stateJson);
68    }
69 
70    std::string targetFile;
71    json::Array errors;
72    json::Array outputs;
73 };
74 
enqueSourceCppStarted()75 void enqueSourceCppStarted()
76 {
77    ClientEvent event(client_events::kSourceCppStarted);
78    module_context::enqueClientEvent(event);
79 }
80 
enqueSourceCppCompleted(const FilePath & sourceFile,const std::string & output,const std::string & errorOutput)81 void enqueSourceCppCompleted(const FilePath& sourceFile,
82                              const std::string& output,
83                              const std::string& errorOutput)
84 {
85    // reset last sourceCpp state with new data
86    using namespace module_context;
87    SourceCppState sourceCppState;
88    sourceCppState.targetFile = module_context::createAliasedPath(sourceFile);
89    sourceCppState.addOutput(kCompileOutputNormal, output);
90    sourceCppState.addOutput(kCompileOutputError, errorOutput);
91 
92    // parse errors
93    std::string allOutput = output + "\n" + errorOutput;
94    CompileErrorParser errorParser = gccErrorParser(sourceFile.getParent());
95    std::vector<SourceMarker> errors = errorParser(allOutput);
96    sourceCppState.errors = sourceMarkersAsJson(errors);
97 
98    // enque event
99    ClientEvent event(client_events::kSourceCppCompleted,
100                      sourceCppState.asJson());
101    module_context::enqueClientEvent(event);
102 }
103 
104 
105 class SourceCppContext : boost::noncopyable
106 {
107 private:
SourceCppContext()108    SourceCppContext() {}
109    friend SourceCppContext& sourceCppContext();
110 
111 public:
onBuild(const FilePath & sourceFile,bool fromCode,bool showOutput)112    bool onBuild(const FilePath& sourceFile, bool fromCode, bool showOutput)
113    {
114       // always clear state before starting a new build
115       reset();
116 
117       // capture params
118       sourceFile_ = sourceFile;
119       fromCode_ = fromCode;
120       showOutput_ = showOutput;
121 
122       // fixup path if necessary
123       std::string path = core::system::getenv("PATH");
124       std::string newPath = path;
125       if (module_context::addRtoolsToPathIfNecessary(&newPath, &rToolsWarning_))
126       {
127           previousPath_ = path;
128           core::system::setenv("PATH", newPath);
129       }
130 
131       // capture all output that goes to the console
132       module_context::events().onConsoleOutput.connect(
133             boost::bind(&SourceCppContext::onConsoleOutput, this, _1, _2));
134 
135       // enque build started
136       enqueSourceCppStarted();
137 
138       // return true to indicate it's okay to build
139       return true;
140    }
141 
onBuildComplete(bool succeeded,const std::string & output)142    void onBuildComplete(bool succeeded, const std::string& output)
143    {
144       // defer handling of build complete so we make sure to get all of the
145       // stderr output from console std stream capture
146       module_context::scheduleDelayedWork(
147                boost::posix_time::milliseconds(200),
148                boost::bind(&SourceCppContext::handleBuildComplete,
149                            this, succeeded, output),
150                true); // idle only
151    }
152 
153 private:
154 
handleBuildComplete(bool succeeded,const std::string & output)155    void handleBuildComplete(bool succeeded, const std::string& output)
156    {
157       // restore previous path
158       if (!previousPath_.empty())
159          core::system::setenv("PATH", previousPath_);
160 
161       // collect all build output (do this before r tools warning so
162       // it's output doesn't end up in consoleErrorBuffer_)
163       std::string buildOutput;
164       if (!succeeded || showOutput_)
165          buildOutput = consoleOutputBuffer_;
166       else
167          buildOutput = output;
168 
169       // if we failed and there was an R tools warning then show it
170       if (!succeeded)
171       {
172          if (!rToolsWarning_.empty())
173             module_context::consoleWriteError(rToolsWarning_);
174 
175          // prompted install of Rtools on Win32
176 #ifdef _WIN32
177          if (!module_context::canBuildCpp())
178             module_context::installRBuildTools("Compiling C/C++ code for R");
179 #endif
180       }
181 
182       // parse for gcc errors for sourceCpp
183       if (!fromCode_)
184          enqueSourceCppCompleted(sourceFile_, buildOutput, consoleErrorBuffer_);
185 
186       // reset state
187       reset();
188    }
189 
190 
onConsoleOutput(module_context::ConsoleOutputType type,std::string output)191    void onConsoleOutput(module_context::ConsoleOutputType type,
192                         std::string output)
193    {
194 #ifdef _WIN32
195       // on windows make sure that output ends with a newline (because
196       // standard output and error both come in on the same channel not
197       // separated by newlines which prevents us from parsing errors)
198       if (!boost::algorithm::ends_with(output, "\n"))
199          output += "\n";
200 #endif
201 
202       if (type == module_context::ConsoleOutputNormal)
203          consoleOutputBuffer_.append(output);
204       else
205          consoleErrorBuffer_.append(output);
206    }
207 
reset()208    void reset()
209    {
210       sourceFile_ = FilePath();
211       showOutput_ = false;
212       fromCode_ = false;
213       consoleOutputBuffer_.clear();
214       consoleErrorBuffer_.clear();
215       module_context::events().onConsoleOutput.disconnect(
216          boost::bind(&SourceCppContext::onConsoleOutput, this, _1, _2));
217       previousPath_.clear();
218       rToolsWarning_.clear();
219    }
220 
221 private:
222    FilePath sourceFile_;
223    bool showOutput_;
224    bool fromCode_;
225    std::string consoleOutputBuffer_;
226    std::string consoleErrorBuffer_;
227    std::string previousPath_;
228    std::string rToolsWarning_;
229 };
230 
sourceCppContext()231 SourceCppContext& sourceCppContext()
232 {
233    static SourceCppContext instance;
234    return instance;
235 }
236 
237 
238 
rs_sourceCppOnBuild(SEXP sFile,SEXP sFromCode,SEXP sShowOutput)239 SEXP rs_sourceCppOnBuild(SEXP sFile, SEXP sFromCode, SEXP sShowOutput)
240 {
241    std::string file = r::sexp::asString(sFile);
242    FilePath filePath(string_utils::systemToUtf8(file));
243    bool fromCode = r::sexp::asLogical(sFromCode);
244    bool showOutput = r::sexp::asLogical(sShowOutput);
245 
246    bool doBuild = sourceCppContext().onBuild(filePath, fromCode, showOutput);
247 
248    r::sexp::Protect rProtect;
249    return r::sexp::create(doBuild, &rProtect);
250 }
251 
rs_sourceCppOnBuildComplete(SEXP sSucceeded,SEXP sOutput)252 SEXP rs_sourceCppOnBuildComplete(SEXP sSucceeded, SEXP sOutput)
253 {
254    bool succeeded = r::sexp::asLogical(sSucceeded);
255 
256    std::string output;
257    if (sOutput != R_NilValue)
258    {
259       std::vector<std::string> outputLines;
260       Error error = r::sexp::extract(sOutput, &outputLines);
261       if (error)
262          LOG_ERROR(error);
263       output = boost::algorithm::join(outputLines, "\n");
264    }
265 
266    sourceCppContext().onBuildComplete(succeeded, output);
267 
268    return R_NilValue;
269 }
270 
271 
272 } // anonymous namespace
273 
274 
initialize()275 Error initialize()
276 {
277    RS_REGISTER_CALL_METHOD(rs_sourceCppOnBuild);
278    RS_REGISTER_CALL_METHOD(rs_sourceCppOnBuildComplete);
279    return Success();
280 }
281 
282 } // namespace source_cpp
283 } // namespace build
284 } // namespace modules
285 } // namespace session
286 } // namespace rstudio
287