1 /*
2  * RSession.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 #define R_INTERNAL_FUNCTIONS
17 #include <r/session/RSession.hpp>
18 
19 #include <iostream>
20 
21 #include <boost/algorithm/string/predicate.hpp>
22 #include <boost/algorithm/string/replace.hpp>
23 #include <boost/algorithm/string/trim.hpp>
24 
25 #include <shared_core/Error.hpp>
26 #include <core/Log.hpp>
27 #include <core/Settings.hpp>
28 #include <core/Scope.hpp>
29 #include <core/system/Architecture.hpp>
30 #include <core/system/System.hpp>
31 #include <core/system/Environment.hpp>
32 #include <core/FileSerializer.hpp>
33 #include <core/FileUtils.hpp>
34 #include <core/http/Util.hpp>
35 #include <core/http/URL.hpp>
36 
37 #include <r/RExec.hpp>
38 #include <r/RUtil.hpp>
39 #include <r/RErrorCategory.hpp>
40 #include <r/ROptions.hpp>
41 #include <r/RRoutines.hpp>
42 #include <r/RInterface.hpp>
43 #include <r/RFunctionHook.hpp>
44 #include <r/RSourceManager.hpp>
45 #include <r/session/RSessionState.hpp>
46 #include <r/session/RClientState.hpp>
47 #include <r/session/RConsoleHistory.hpp>
48 #include <r/session/RDiscovery.hpp>
49 
50 #include "RClientMetrics.hpp"
51 #include "REmbedded.hpp"
52 #include "RInit.hpp"
53 #include "RQuit.hpp"
54 #include "RRestartContext.hpp"
55 #include "RStdCallbacks.hpp"
56 #include "RScriptCallbacks.hpp"
57 #include "RSuspend.hpp"
58 
59 #include "graphics/RGraphicsDevDesc.hpp"
60 #include "graphics/RGraphicsUtils.hpp"
61 #include "graphics/RGraphicsDevice.hpp"
62 #include "graphics/RGraphicsPlotManager.hpp"
63 
64 #include <Rembedded.h>
65 #include <R_ext/Utils.h>
66 #include <R_ext/Rdynload.h>
67 #include <R_ext/RStartup.h>
68 
69 #include <gsl/gsl>
70 
71 extern "C" {
72 int Rf_countContexts(int, int);
73 }
74 #define CTXT_BROWSER 16
75 
76 // get rid of windows TRUE and FALSE definitions
77 #undef TRUE
78 #undef FALSE
79 
80 using namespace rstudio::core;
81 
82 namespace rstudio {
83 namespace r {
84 namespace session {
85 
86 namespace {
87 
88 // options
89 ROptions s_options;
90 
91 } // anonymous namespace
92 
93 
94 const int kSerializationActionSaveDefaultWorkspace = 1;
95 const int kSerializationActionLoadDefaultWorkspace = 2;
96 const int kSerializationActionSuspendSession = 3;
97 const int kSerializationActionResumeSession = 4;
98 const int kSerializationActionCompleted = 5;
99 
rs_editFile(SEXP fileSEXP)100 SEXP rs_editFile(SEXP fileSEXP)
101 {
102    try
103    {
104       std::string file = r::sexp::asString(fileSEXP);
105       bool success = REditFile(file.c_str()) == 0;
106       r::sexp::Protect rProtect;
107       return r::sexp::create(success, &rProtect);
108    }
109    catch(r::exec::RErrorException& e)
110    {
111       r::exec::error(e.message());
112    }
113    CATCH_UNEXPECTED_EXCEPTION
114 
115    // keep compiler happy (this code is unreachable)
116    return R_NilValue;
117 }
118 
rs_showFile(SEXP titleSEXP,SEXP fileSEXP,SEXP delSEXP)119 SEXP rs_showFile(SEXP titleSEXP, SEXP fileSEXP, SEXP delSEXP)
120 {
121    try
122    {
123       std::string file = r::util::fixPath(r::sexp::asString(fileSEXP));
124       FilePath filePath = utils::safeCurrentPath().completePath(file);
125       if (!filePath.exists())
126       {
127           throw r::exec::RErrorException(
128                              "File " + file + " does not exist.");
129       }
130 
131       rCallbacks().showFile(r::sexp::asString(titleSEXP),
132                            filePath,
133                            r::sexp::asLogical(delSEXP));
134    }
135    catch(r::exec::RErrorException& e)
136    {
137       r::exec::error(e.message());
138    }
139    CATCH_UNEXPECTED_EXCEPTION
140 
141    return R_NilValue;
142 }
143 
144 // method called from browseUrl
rs_browseURL(SEXP urlSEXP)145 SEXP rs_browseURL(SEXP urlSEXP)
146 {
147    try
148    {
149       std::string URL = r::sexp::asString(urlSEXP);
150 
151       // file urls require special dispatching
152       std::string filePrefix("file://");
153       if (URL.find(filePrefix) == 0)
154       {
155          // also look for file:///c: style urls on windows
156 #ifdef _WIN32
157          if (URL.find(filePrefix + "/") == 0)
158              filePrefix = filePrefix + "/";
159 #endif
160 
161          // transform into FilePath
162          std::string path = URL.substr(filePrefix.length());
163          path = core::http::util::urlDecode(path);
164          FilePath filePath(r::util::fixPath(path));
165 
166          // sometimes R passes short paths (like for files within the
167          // R home directory). Convert these to long paths
168 #ifdef _WIN32
169          core::system::ensureLongPath(&filePath);
170 #endif
171 
172          // fire browseFile
173          rCallbacks().browseFile(filePath);
174       }
175       // urls with no protocol are assumed to be file references
176       else if (URL.find("://") == std::string::npos)
177       {
178          std::string file = r::util::expandFileName(URL);
179          FilePath filePath = utils::safeCurrentPath().completePath(
180             r::util::fixPath(file));
181          rCallbacks().browseFile(filePath);
182       }
183       else
184       {
185          rCallbacks().browseURL(URL);
186       }
187    }
188    CATCH_UNEXPECTED_EXCEPTION
189 
190    return R_NilValue;
191 }
192 
rs_createUUID()193 SEXP rs_createUUID()
194 {
195    r::sexp::Protect rProtect;
196    return r::sexp::create(core::system::generateUuid(false), &rProtect);
197 }
198 
rs_loadHistory(SEXP sFile)199 SEXP rs_loadHistory(SEXP sFile)
200 {
201    std::string file = R_ExpandFileName(r::sexp::asString(sFile).c_str());
202    Error error = consoleHistory().loadFromFile(FilePath(file), true);
203    if (error)
204       LOG_ERROR(error);
205    else
206       rCallbacks().consoleHistoryReset();
207    return R_NilValue;
208 }
209 
rs_saveHistory(SEXP sFile)210 SEXP rs_saveHistory(SEXP sFile)
211 {
212    std::string file = R_ExpandFileName(r::sexp::asString(sFile).c_str());
213    consoleHistory().saveToFile(FilePath(file));
214    return R_NilValue;
215 }
216 
217 
rs_completeUrl(SEXP url,SEXP path)218 SEXP rs_completeUrl(SEXP url, SEXP path)
219 {
220    std::string completed = rstudio::core::http::URL::complete(
221       r::sexp::asString(url), r::sexp::asString(path));
222 
223    r::sexp::Protect rProtect;
224    return r::sexp::create(completed, &rProtect);
225 }
226 
227 namespace {
228 
rs_GEcopyDisplayList(SEXP fromDeviceSEXP)229 SEXP rs_GEcopyDisplayList(SEXP fromDeviceSEXP)
230 {
231    int fromDevice = r::sexp::asInteger(fromDeviceSEXP);
232    GEcopyDisplayList(fromDevice);
233    return Rf_ScalarLogical(1);
234 }
235 
rs_GEplayDisplayList()236 SEXP rs_GEplayDisplayList()
237 {
238    graphics::device::playDisplayList();
239    return Rf_ScalarLogical(1);
240 }
241 
242 #ifdef __APPLE__
243 
validateCompatible(const std::string & rHome)244 Error validateCompatible(const std::string& rHome)
245 {
246    FilePath rsessionPath;
247    Error error = core::system::executablePath(nullptr, &rsessionPath);
248    if (error)
249    {
250       LOG_ERROR(error);
251       return Success();
252    }
253 
254    FilePath rLibPath = FilePath(rHome).completeChildPath("lib/libR.dylib");
255    if (!rLibPath.exists())
256    {
257       LOG_ERROR(fileNotFoundError(rLibPath, ERROR_LOCATION));
258       return Success();
259    }
260 
261    std::string rsessionArchs = core::system::supportedArchitectures(rsessionPath);
262    std::string rArchs = core::system::supportedArchitectures(rLibPath);
263 
264    for (auto arch : { "x86_64", "arm64" })
265    {
266       if (rsessionArchs.find(arch) != std::string::npos &&
267           rArchs.find(arch) != std::string::npos)
268       {
269          return Success();
270       }
271    }
272 
273    Error formatError(boost::system::errc::executable_format_error, ERROR_LOCATION);
274    formatError.addProperty("r-home", rHome);
275    formatError.addProperty("r-archs", rArchs);
276    formatError.addProperty("rsession-archs", rsessionArchs);
277    return formatError;
278 
279 }
280 
281 #endif
282 
283 } // end anonymous namespace
284 
run(const ROptions & options,const RCallbacks & callbacks)285 Error run(const ROptions& options, const RCallbacks& callbacks)
286 {
287    // copy options and callbacks
288    s_options = options;
289    setRCallbacks(callbacks);
290 
291    // set to default "C" numeric locale as-per R embedding docs
292    setlocale(LC_NUMERIC, "C");
293 
294    // perform R discovery
295    r::session::RLocations rLocations;
296    Error error = r::session::discoverR(&rLocations);
297    if (error)
298       return error;
299 
300    // R_HOME
301    core::system::setenv("R_HOME", rLocations.homePath);
302 
303    // R_DOC_DIR (required by help-links.sh)
304    core::system::setenv("R_DOC_DIR", rLocations.docPath);
305 
306    // R_LIBS_USER
307    if (!s_options.rLibsUser.empty())
308       core::system::setenv("R_LIBS_USER", s_options.rLibsUser);
309 
310 #ifdef __APPLE__
311    // validate compatible architecture
312    error = validateCompatible(rLocations.homePath);
313    if (error)
314       return error;
315 #endif
316 
317    // set compatible graphics engine version
318    int engineVersion = s_options.rCompatibleGraphicsEngineVersion;
319    graphics::setCompatibleEngineVersion(engineVersion);
320 
321    // set source reloading behavior
322    sourceManager().setAutoReload(options.autoReloadSource);
323 
324    // initialize suspended session path
325    FilePath userScratch = s_options.userScratchPath;
326    FilePath oldSuspendedSessionPath = userScratch.completePath("suspended-session");
327    FilePath sessionScratch = s_options.sessionScratchPath;
328 
329    // set suspend paths
330    setSuspendPaths(
331       sessionScratch.completePath("suspended-session-data"),              // session data
332       s_options.userScratchPath.completePath("client-state"),             // client state
333       s_options.scopedScratchPath.completePath("pcs"));                   // project client state
334 
335    // one time migration of global suspend to default project suspend
336    if (!suspendedSessionPath().exists() && oldSuspendedSessionPath.exists())
337    {
338      // try to move it first
339      Error error = oldSuspendedSessionPath.move(suspendedSessionPath());
340      if (error)
341      {
342         // log the move error
343         LOG_ERROR(error);
344 
345         // try to copy it as a failsafe (eliminates cross-volume issues)
346         error = file_utils::copyDirectory(oldSuspendedSessionPath,
347                                                 suspendedSessionPath());
348         if (error)
349            LOG_ERROR(error);
350 
351         // remove so this is always a one-time only thing
352         error = oldSuspendedSessionPath.remove();
353         if (error)
354            LOG_ERROR(error);
355      }
356    }
357 
358    // initialize restart context
359    restartContext().initialize(s_options.scopedScratchPath,
360                                s_options.sessionPort);
361 
362    // register methods
363    RS_REGISTER_CALL_METHOD(rs_browseURL);
364    RS_REGISTER_CALL_METHOD(rs_editFile);
365    RS_REGISTER_CALL_METHOD(rs_showFile);
366    RS_REGISTER_CALL_METHOD(rs_createUUID);
367    RS_REGISTER_CALL_METHOD(rs_loadHistory);
368    RS_REGISTER_CALL_METHOD(rs_saveHistory);
369    RS_REGISTER_CALL_METHOD(rs_completeUrl);
370    RS_REGISTER_CALL_METHOD(rs_GEcopyDisplayList, 1);
371    RS_REGISTER_CALL_METHOD(rs_GEplayDisplayList, 0);
372 
373    // run R
374 
375    // should we run .Rprofile?
376    bool loadInitFile = false;
377    if (restartContext().hasSessionState())
378    {
379       loadInitFile = restartContext().rProfileOnRestore() && !options.disableRProfileOnStart;
380    }
381    else
382    {
383       // we run the .Rprofile if this is a brand new session and
384       // we are in a project and the DisableExecuteProfile setting is not set, or we are not in a project
385       // alternatively, if we are resuming a session and the option is set to possibly run the .Rprofile
386       // we will only run it if the DisableExecuteProfile project setting is not set (or we are not in a project)
387       // finally, if this is a packrat project, we always run the Rprofile as it is required for correct operation
388       loadInitFile = (!options.disableRProfileOnStart && (!suspendedSessionPath().exists() || options.rProfileOnResume))
389                      || options.packratEnabled
390                      || r::session::state::packratModeEnabled(suspendedSessionPath());
391    }
392 
393    // quiet for resume cases
394    bool quiet = restartContext().hasSessionState() ||
395                 suspendedSessionPath().exists();
396 
397    r::session::Callbacks cb;
398    if (options.runScript.empty())
399    {
400       // normal session: read/write from browser
401       cb.readConsole = RReadConsole;
402       cb.writeConsoleEx = RWriteConsoleEx;
403       cb.cleanUp = RCleanUp;
404    }
405    else
406    {
407       // headless script execution: read/write from script and output to stdout
408       setRunScript(options.runScript);
409       cb.readConsole = RReadScript;
410       cb.writeConsoleEx = RWriteStdout;
411       cb.cleanUp = RScriptCleanUp;
412    }
413 
414    cb.showMessage = RShowMessage;
415    cb.editFile = REditFile;
416    cb.busy = RBusy;
417    cb.chooseFile = RChooseFile;
418    cb.showFiles = RShowFiles;
419    cb.loadhistory = Rloadhistory;
420    cb.savehistory = Rsavehistory;
421    cb.addhistory = Raddhistory;
422    cb.suicide = RSuicide;
423    r::session::runEmbeddedR(FilePath(rLocations.homePath),
424                             options.userHomePath,
425                             quiet,
426                             loadInitFile,
427                             s_options.saveWorkspace,
428                             cb,
429                             stdInternalCallbacks());
430 
431    // keep compiler happy
432    return Success();
433 }
434 
435 namespace {
436 
doSetClientMetrics(const RClientMetrics & metrics)437 void doSetClientMetrics(const RClientMetrics& metrics)
438 {
439    // set the metrics
440    client_metrics::set(metrics);
441 }
442 
443 } // anonymous namespace
444 
setClientMetrics(const RClientMetrics & metrics)445 void setClientMetrics(const RClientMetrics& metrics)
446 {
447    // get existing values in case this results in an error
448    RClientMetrics previousMetrics = client_metrics::get();
449 
450    // attempt to set the metrics
451    Error error = r::exec::executeSafely(boost::bind(doSetClientMetrics,
452                                                     metrics));
453 
454    if (error)
455    {
456       // report to user
457       std::string errMsg = r::endUserErrorMessage(error);
458       REprintf("%s\n", errMsg.c_str());
459 
460       // restore previous values (but don't fire plotsChanged b/c
461       // the reset doesn't result in a change in graphics state)
462       r::exec::executeSafely(boost::bind(doSetClientMetrics, previousMetrics));
463    }
464 }
465 
reportAndLogWarning(const std::string & warning)466 void reportAndLogWarning(const std::string& warning)
467 {
468    std::string msg = "WARNING: " + warning + "\n";
469    RWriteConsoleEx(msg.c_str(), gsl::narrow_cast<int>(msg.length()), 1);
470    LOG_WARNING_MESSAGE("(Reported to User) " + warning);
471 }
472 
isSuspendable(const std::string & currentPrompt)473 bool isSuspendable(const std::string& currentPrompt)
474 {
475    // NOTE: active file graphics devices (e.g. png or pdf) are wiped out
476    // during a suspend as are open connections. there may or may not be a
477    // way to make this more robust.
478 
479    if (s_options.suspendOnIncompleteStatement)
480    {
481       // Always allow suspending, even if the statement is not complete.
482       return true;
483    }
484    else
485    {
486       // Avoid suspending when the prompt is not at its default value.  This mostly prevents us from
487       // suspending if the user hasn't finished an R statement, since R's prompt changes from > to +
488       // when a statement is incomplete. It is an option since some environments prefer more
489       // aggressive suspension behavior.
490       std::string defaultPrompt = r::options::getOption<std::string>("prompt");
491       if (currentPrompt != defaultPrompt)
492       {
493          return false;
494       }
495    }
496 
497    return true;
498 }
499 
500 
browserContextActive()501 bool browserContextActive()
502 {
503    return Rf_countContexts(CTXT_BROWSER, 1) > 0;
504 }
505 
506 namespace utils {
507 
isPackratModeOn()508 bool isPackratModeOn()
509 {
510    return !core::system::getenv("R_PACKRAT_MODE").empty();
511 }
512 
isDevtoolsDevModeOn()513 bool isDevtoolsDevModeOn()
514 {
515    bool isDevtoolsDevModeOn = false;
516    Error error = r::exec::RFunction(".rs.devModeOn").call(&isDevtoolsDevModeOn);
517    if (error)
518       LOG_ERROR(error);
519    return isDevtoolsDevModeOn;
520 }
521 
isDefaultPrompt(const std::string & prompt)522 bool isDefaultPrompt(const std::string& prompt)
523 {
524    return prompt == r::options::getOption<std::string>("prompt");
525 }
526 
isServerMode()527 bool isServerMode()
528 {
529    return s_options.serverMode;
530 }
531 
userHomePath()532 const FilePath& userHomePath()
533 {
534    return s_options.userHomePath;
535 }
536 
logPath()537 FilePath logPath()
538 {
539    return s_options.logPath;
540 }
541 
sessionScratchPath()542 FilePath sessionScratchPath()
543 {
544    return s_options.sessionScratchPath;
545 }
546 
scopedScratchPath()547 FilePath scopedScratchPath()
548 {
549    return s_options.scopedScratchPath;
550 }
551 
safeCurrentPath()552 FilePath safeCurrentPath()
553 {
554    return FilePath::safeCurrentPath(userHomePath());
555 }
556 
rHistoryDir()557 FilePath rHistoryDir()
558 {
559    return s_options.rHistoryDir();
560 }
561 
rEnvironmentDir()562 FilePath rEnvironmentDir()
563 {
564    return s_options.rEnvironmentDir();
565 }
566 
startupEnvironmentFilePath()567 FilePath startupEnvironmentFilePath()
568 {
569    return s_options.startupEnvironmentFilePath;
570 }
571 
rSourcePath()572 FilePath rSourcePath()
573 {
574    return s_options.rSourcePath;
575 }
576 
restoreWorkspace()577 bool restoreWorkspace()
578 {
579    return s_options.restoreWorkspace;
580 }
581 
sessionPort()582 std::string sessionPort()
583 {
584    return s_options.sessionPort;
585 }
586 
rCRANUrl()587 std::string rCRANUrl()
588 {
589    return s_options.rCRANUrl;
590 }
591 
rCRANSecondary()592 std::string rCRANSecondary()
593 {
594    return s_options.rCRANSecondary;
595 }
596 
useInternet2()597 bool useInternet2()
598 {
599    return s_options.useInternet2;
600 }
601 
alwaysSaveHistory()602 bool alwaysSaveHistory()
603 {
604    return s_options.alwaysSaveHistory();
605 }
606 
restoreEnvironmentOnResume()607 bool restoreEnvironmentOnResume()
608 {
609    return s_options.restoreEnvironmentOnResume;
610 }
611 
tempFile(const std::string & prefix,const std::string & extension)612 FilePath tempFile(const std::string& prefix, const std::string& extension)
613 {
614    std::string filename;
615    Error error = r::exec::RFunction("tempfile", prefix).call(&filename);
616    if (error)
617       LOG_ERROR(error);
618    FilePath filePath(string_utils::systemToUtf8(r::util::fixPath(filename)) + "." + extension);
619    return filePath;
620 }
621 
tempDir()622 FilePath tempDir()
623 {
624    std::string tempDir;
625    Error error = r::exec::RFunction("tempdir").call(&tempDir);
626    if (error)
627       LOG_ERROR(error);
628 
629    FilePath filePath(string_utils::systemToUtf8(r::util::fixPath(tempDir)));
630    return filePath;
631 }
632 
633 } // namespace utils
634 
635 } // namespace session
636 } // namespace r
637 } // namespace rstudio
638