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