/*
* SessionModuleContext.cpp
*
* Copyright (C) 2009-20 by RStudio, PBC
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
#include "SessionModuleContextInternal.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "SessionClientEventQueue.hpp"
#include
#include
#include
#include
#include
#include
#include "modules/SessionBreakpoints.hpp"
#include "modules/SessionVCS.hpp"
#include "modules/SessionFiles.hpp"
#include "session-config.h"
using namespace rstudio::core ;
namespace rstudio {
namespace session {
namespace module_context {
namespace {
// simple service for handling console_input rpc requests
class ConsoleInputService : boost::noncopyable
{
public:
ConsoleInputService()
{
core::thread::safeLaunchThread(
boost::bind(&ConsoleInputService::run, this),
&thread_);
}
~ConsoleInputService()
{
enqueue("!");
}
void enqueue(const std::string& input)
{
requests_.enque(input);
}
private:
void run()
{
while (true)
{
std::string input;
while (requests_.deque(&input))
{
if (input == "!")
return;
core::http::Response response;
Error error = session::http::sendSessionRequest(
"/rpc/console_input",
input,
&response);
if (error)
LOG_ERROR(error);
}
requests_.wait();
}
}
boost::thread thread_;
core::thread::ThreadsafeQueue requests_;
};
ConsoleInputService& consoleInputService()
{
static ConsoleInputService instance;
return instance;
}
// enqueClientEvent from R
SEXP rs_enqueClientEvent(SEXP nameSEXP, SEXP dataSEXP)
{
try
{
// extract name
std::string name = r::sexp::asString(nameSEXP);
// extract json value (for primitive types we only support scalars
// since this is the most common type of event data). to return an
// array of primitives you need to wrap them in a list/object
Error extractError ;
json::Value data ;
switch(TYPEOF(dataSEXP))
{
case NILSXP:
{
// do nothing, data will be a null json value
break;
}
case VECSXP:
{
extractError = r::json::jsonValueFromList(dataSEXP, &data);
break;
}
default:
{
extractError = r::json::jsonValueFromScalar(dataSEXP, &data);
break;
}
}
// check for error
if (extractError)
{
LOG_ERROR(extractError);
throw r::exec::RErrorException(
"Couldn't extract json value from event data");
}
// determine the event type from the event name
int type = -1 ;
if (name == "package_status_changed")
type = session::client_events::kPackageStatusChanged;
else if (name == "unhandled_error")
type = session::client_events::kUnhandledError;
else if (name == "enable_rstudio_connect")
type = session::client_events::kEnableRStudioConnect;
else if (name == "shiny_gadget_dialog")
type = session::client_events::kShinyGadgetDialog;
else if (name == "rmd_params_ready")
type = session::client_events::kRmdParamsReady;
else if (name == "jump_to_function")
type = session::client_events::kJumpToFunction;
else if (name == "send_to_console")
type = session::client_events::kSendToConsole;
else if (name == "rprof_started")
type = session::client_events::kRprofStarted;
else if (name == "rprof_stopped")
type = session::client_events::kRprofStopped;
else if (name == "rprof_created")
type = session::client_events::kRprofCreated;
else if (name == "editor_command")
type = session::client_events::kEditorCommand;
else if (name == "navigate_shiny_frame")
type = session::client_events::kNavigateShinyFrame;
else if (name == "update_new_connection_dialog")
type = session::client_events::kUpdateNewConnectionDialog;
else if (name == "terminal_subprocs")
type = session::client_events::kTerminalSubprocs;
else if (name == "rstudioapi_show_dialog")
type = session::client_events::kRStudioAPIShowDialog;
else if (name == "object_explorer_event")
type = session::client_events::kObjectExplorerEvent;
else if (name == "send_to_terminal")
type = session::client_events::kSendToTerminal;
else if (name == "clear_terminal")
type = session::client_events::kClearTerminal;
else if (name == "add_terminal")
type = session::client_events::kAddTerminal;
else if (name == "activate_terminal")
type = session::client_events::kActivateTerminal;
else if (name == "terminal_cwd")
type = session::client_events::kTerminalCwd;
else if (name == "remove_terminal")
type = session::client_events::kRemoveTerminal;
else if (name == "show_page_viewer")
type = session::client_events::kShowPageViewerEvent;
else if (name == "data_output_completed")
type = session::client_events::kDataOutputCompleted;
else if (name == "new_document_with_code")
type = session::client_events::kNewDocumentWithCode;
else if (name == "available_packages_ready")
type = session::client_events::kAvailablePackagesReady;
else if (name == "compute_theme_colors")
type = session::client_events::kComputeThemeColors;
else if (name == "tutorial_command")
type = session::client_events::kTutorialCommand;
else if (name == "tutorial_launch")
type = session::client_events::kTutorialLaunch;
if (type != -1)
{
ClientEvent event(type, data);
session::clientEventQueue().add(event);
}
else
{
LOG_ERROR_MESSAGE("Unexpected event name from R: " + name);
}
}
catch(r::exec::RErrorException& e)
{
r::exec::error(e.message());
}
CATCH_UNEXPECTED_EXCEPTION
return R_NilValue ;
}
SEXP rs_activatePane(SEXP paneSEXP)
{
module_context::activatePane(r::sexp::safeAsString(paneSEXP));
return R_NilValue;
}
// show error message from R
SEXP rs_showErrorMessage(SEXP titleSEXP, SEXP messageSEXP)
{
std::string title = r::sexp::asString(titleSEXP);
std::string message = r::sexp::asString(messageSEXP);
module_context::showErrorMessage(title, message);
return R_NilValue;
}
// log error message from R
SEXP rs_logErrorMessage(SEXP messageSEXP)
{
std::string message = r::sexp::asString(messageSEXP);
LOG_ERROR_MESSAGE(message);
return R_NilValue;
}
// log warning message from R
SEXP rs_logWarningMessage(SEXP messageSEXP)
{
std::string message = r::sexp::asString(messageSEXP);
LOG_WARNING_MESSAGE(message);
return R_NilValue;
}
// sleep the main thread (debugging function used to test rpc/abort)
SEXP rs_threadSleep(SEXP secondsSEXP)
{
int seconds = r::sexp::asInteger(secondsSEXP);
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
return R_NilValue;
}
// get rstudio mode
SEXP rs_rstudioProgramMode()
{
r::sexp::Protect rProtect;
return r::sexp::create(session::options().programMode(), &rProtect);
}
// get rstudio edition
SEXP rs_rstudioEdition()
{
return R_NilValue;
}
// get version
SEXP rs_rstudioVersion()
{
r::sexp::Protect rProtect;
return r::sexp::create(std::string(RSTUDIO_VERSION), &rProtect);
}
// get release name
SEXP rs_rstudioReleaseName()
{
r::sexp::Protect rProtect;
return r::sexp::create(std::string(RSTUDIO_RELEASE_NAME), &rProtect);
}
// get citation
SEXP rs_rstudioCitation()
{
FilePath resPath = session::options().rResourcesPath();
FilePath citationPath = resPath.completeChildPath("CITATION");
// the citation file may not exist when working in e.g.
// development configurations so just ignore if it's missing
if (!citationPath.exists())
return R_NilValue;
SEXP citationSEXP;
r::sexp::Protect rProtect;
Error error = r::exec::RFunction("utils:::readCitationFile",
citationPath.getAbsolutePath())
.call(&citationSEXP,
&rProtect);
if (error)
{
LOG_ERROR(error);
return R_NilValue;
}
else
{
return citationSEXP;
}
}
SEXP rs_setUsingMingwGcc49(SEXP usingSEXP)
{
bool usingMingwGcc49 = r::sexp::asLogical(usingSEXP);
prefs::userState().setUsingMingwGcc49(usingMingwGcc49);
return R_NilValue;
}
// ensure file hidden
SEXP rs_ensureFileHidden(SEXP fileSEXP)
{
#ifdef _WIN32
std::string file = r::sexp::asString(fileSEXP);
if (!file.empty())
{
FilePath filePath = module_context::resolveAliasedPath(file);
Error error = core::system::makeFileHidden(filePath);
if (error)
LOG_ERROR(error);
}
#endif
return R_NilValue;
}
SEXP rs_sourceDiagnostics()
{
module_context::sourceDiagnostics();
return R_NilValue;
}
SEXP rs_packageLoaded(SEXP pkgnameSEXP)
{
std::string pkgname = r::sexp::safeAsString(pkgnameSEXP);
// fire server event
events().onPackageLoaded(pkgname);
// fire client event
ClientEvent packageLoadedEvent(
client_events::kPackageLoaded,
json::Value(pkgname));
enqueClientEvent(packageLoadedEvent);
return R_NilValue;
}
SEXP rs_packageUnloaded(SEXP pkgnameSEXP)
{
std::string pkgname = r::sexp::safeAsString(pkgnameSEXP);
ClientEvent packageUnloadedEvent(
client_events::kPackageUnloaded,
json::Value(pkgname));
enqueClientEvent(packageUnloadedEvent);
return R_NilValue;
}
SEXP rs_userPrompt(SEXP typeSEXP,
SEXP captionSEXP,
SEXP messageSEXP,
SEXP yesLabelSEXP,
SEXP noLabelSEXP,
SEXP includeCancelSEXP,
SEXP yesIsDefaultSEXP)
{
UserPrompt prompt(r::sexp::asInteger(typeSEXP),
r::sexp::safeAsString(captionSEXP),
r::sexp::safeAsString(messageSEXP),
r::sexp::safeAsString(yesLabelSEXP),
r::sexp::safeAsString(noLabelSEXP),
r::sexp::asLogical(includeCancelSEXP),
r::sexp::asLogical(yesIsDefaultSEXP));
UserPrompt::Response response = showUserPrompt(prompt);
r::sexp::Protect rProtect;
return r::sexp::create(response, &rProtect);
}
SEXP rs_restartR(SEXP afterRestartSEXP)
{
std::string afterRestart = r::sexp::safeAsString(afterRestartSEXP);
json::Object dataJson;
dataJson["after_restart"] = afterRestart;
ClientEvent event(client_events::kSuspendAndRestart, dataJson);
module_context::enqueClientEvent(event);
return R_NilValue;
}
SEXP rs_generateShortUuid()
{
// generate a short uuid -- we make this available in R code so that it's
// possible to create random identifiers without perturbing the state of the
// RNG that R uses
std::string uuid = core::system::generateShortenedUuid();
r::sexp::Protect rProtect;
return r::sexp::create(uuid, &rProtect);
}
SEXP rs_markdownToHTML(SEXP contentSEXP)
{
std::string content = r::sexp::safeAsString(contentSEXP);
std::string htmlContent;
Error error = markdown::markdownToHTML(content,
markdown::Extensions(),
markdown::HTMLOptions(),
&htmlContent);
if (error)
{
LOG_ERROR(error);
htmlContent = content;
}
r::sexp::Protect rProtect;
return r::sexp::create(htmlContent, &rProtect);
}
inline std::string persistantValueName(SEXP nameSEXP)
{
return "rstudioapi_persistent_values_" + r::sexp::safeAsString(nameSEXP);
}
SEXP rs_setPersistentValue(SEXP nameSEXP, SEXP valueSEXP)
{
std::string name = persistantValueName(nameSEXP);
std::string value = r::sexp::safeAsString(valueSEXP);
persistentState().settings().set(name, value);
return R_NilValue;
}
SEXP rs_getPersistentValue(SEXP nameSEXP)
{
std::string name = persistantValueName(nameSEXP);
if (persistentState().settings().contains(name))
{
std::string value = persistentState().settings().get(name);
r::sexp::Protect rProtect;
return r::sexp::create(value, &rProtect);
}
else
{
return R_NilValue;
}
}
} // anonymous namespace
// register a scratch path which is monitored
namespace {
typedef std::map MonitoredScratchPaths;
MonitoredScratchPaths s_monitoredScratchPaths;
bool s_monitorByScanning = false;
FilePath monitoredParentPath()
{
FilePath monitoredPath = userScratchPath().completePath(kMonitoredPath);
Error error = monitoredPath.ensureDirectory();
if (error)
LOG_ERROR(error);
return monitoredPath;
}
bool monitoredScratchFilter(const FileInfo& fileInfo)
{
return true;
}
void onFilesChanged(const std::vector& changes)
{
for (const core::system::FileChangeEvent& fileChange : changes)
{
FilePath changedFilePath(fileChange.fileInfo().absolutePath());
for (MonitoredScratchPaths::const_iterator
it = s_monitoredScratchPaths.begin();
it != s_monitoredScratchPaths.end();
++it)
{
if (changedFilePath.isWithin(it->first))
{
it->second(fileChange);
break;
}
}
}
}
boost::shared_ptr monitoredPathTree()
{
boost::shared_ptr pMonitoredTree(new tree());
core::system::FileScannerOptions options;
options.recursive = true;
options.filter = monitoredScratchFilter;
Error scanError = scanFiles(FileInfo(monitoredParentPath()),
options,
pMonitoredTree.get());
if (scanError)
LOG_ERROR(scanError);
return pMonitoredTree;
}
bool scanForMonitoredPathChanges(boost::shared_ptr pPrevTree)
{
// check for changes
std::vector changes;
boost::shared_ptr pCurrentTree = monitoredPathTree();
core::system::collectFileChangeEvents(pPrevTree->begin(),
pPrevTree->end(),
pCurrentTree->begin(),
pCurrentTree->end(),
&changes);
// fire events
onFilesChanged(changes);
// reset the tree
*pPrevTree = *pCurrentTree;
// scan again after interval
return true;
}
void onMonitoringError(const Error& error)
{
// log the error
LOG_ERROR(error);
// fallback to periodically scanning for changes
if (!s_monitorByScanning)
{
s_monitorByScanning = true;
module_context::schedulePeriodicWork(
boost::posix_time::seconds(3),
boost::bind(scanForMonitoredPathChanges, monitoredPathTree()),
true);
}
}
void initializeMonitoredUserScratchDir()
{
// setup callbacks and register
core::system::file_monitor::Callbacks cb;
cb.onRegistrationError = onMonitoringError;
cb.onMonitoringError = onMonitoringError;
cb.onFilesChanged = onFilesChanged;
core::system::file_monitor::registerMonitor(
monitoredParentPath(),
true,
monitoredScratchFilter,
cb);
}
} // anonymous namespace
FilePath registerMonitoredUserScratchDir(const std::string& dirName,
const OnFileChange& onFileChange)
{
// create the subdir
FilePath dirPath = monitoredParentPath().completePath(dirName);
Error error = dirPath.ensureDirectory();
if (error)
LOG_ERROR(error);
// register the path
s_monitoredScratchPaths[dirPath] = onFileChange;
// return it
return dirPath;
}
namespace {
// manage signals used for custom save and restore
class SuspendHandlers : boost::noncopyable
{
public:
SuspendHandlers() : nextGroup_(0) {}
public:
void add(const SuspendHandler& handler)
{
int group = nextGroup_++;
suspendSignal_.connect(group, handler.suspend());
resumeSignal_.connect(group, handler.resume());
}
void suspend(const r::session::RSuspendOptions& options,
Settings* pSettings)
{
suspendSignal_(options, pSettings);
}
void resume(const Settings& settings)
{
resumeSignal_(settings);
}
private:
// use groups to ensure signal order. call suspend handlers in order
// of subscription and call resume handlers in reverse order of
// subscription.
int nextGroup_;
RSTUDIO_BOOST_SIGNAL suspendSignal_;
RSTUDIO_BOOST_SIGNAL resumeSignal_;
};
// handlers instance
SuspendHandlers& suspendHandlers()
{
static SuspendHandlers instance;
return instance;
}
} // anonymous namespace
void addSuspendHandler(const SuspendHandler& handler)
{
suspendHandlers().add(handler);
}
void onSuspended(const r::session::RSuspendOptions& options,
Settings* pPersistentState)
{
pPersistentState->beginUpdate();
suspendHandlers().suspend(options, pPersistentState);
pPersistentState->endUpdate();
}
void onResumed(const Settings& persistentState)
{
suspendHandlers().resume(persistentState);
}
// idle work
namespace {
typedef std::vector
ScheduledCommands;
ScheduledCommands s_scheduledCommands;
ScheduledCommands s_idleScheduledCommands;
void addScheduledCommand(boost::shared_ptr pCommand,
bool idleOnly)
{
if (idleOnly)
s_idleScheduledCommands.push_back(pCommand);
else
s_scheduledCommands.push_back(pCommand);
}
void executeScheduledCommands(ScheduledCommands* pCommands)
{
// make a copy of scheduled commands before executing them
// (this is because a scheduled command could result in
// R code executing which in turn could cause the list of
// scheduled commands to be mutated and these iterators
// invalidated)
ScheduledCommands commands = *pCommands;
// execute all commands
std::for_each(commands.begin(),
commands.end(),
boost::bind(&ScheduledCommand::execute, _1));
// remove any commands which are finished
pCommands->erase(
std::remove_if(
pCommands->begin(),
pCommands->end(),
boost::bind(&ScheduledCommand::finished, _1)),
pCommands->end());
}
} // anonymous namespace
void scheduleIncrementalWork(
const boost::posix_time::time_duration& incrementalDuration,
const boost::function& execute,
bool idleOnly)
{
addScheduledCommand(boost::shared_ptr(
new IncrementalCommand(incrementalDuration,
execute)),
idleOnly);
}
void scheduleIncrementalWork(
const boost::posix_time::time_duration& initialDuration,
const boost::posix_time::time_duration& incrementalDuration,
const boost::function& execute,
bool idleOnly)
{
addScheduledCommand(boost::shared_ptr(
new IncrementalCommand(initialDuration,
incrementalDuration,
execute)),
idleOnly);
}
void schedulePeriodicWork(const boost::posix_time::time_duration& period,
const boost::function &execute,
bool idleOnly,
bool immediate)
{
addScheduledCommand(boost::shared_ptr(
new PeriodicCommand(period, execute, immediate)),
idleOnly);
}
namespace {
bool performDelayedWork(const boost::function &execute,
boost::shared_ptr pExecuted)
{
if (*pExecuted)
return false;
*pExecuted = true;
execute();
return false;
}
bool isPackagePosixMakefile(const FilePath& srcPath)
{
if (!srcPath.exists())
return false;
using namespace projects;
ProjectContext& context = session::projects::projectContext();
if (!context.hasProject())
return false;
if (context.config().buildType != r_util::kBuildTypePackage)
return false;
FilePath parentDir = srcPath.getParent();
if (parentDir.getFilename() != "src")
return false;
FilePath packagePath = context.buildTargetPath();
if (parentDir.getParent() != packagePath)
return false;
std::string filename = srcPath.getFilename();
return (filename == "Makevars" ||
filename == "Makevars.in" ||
filename == "Makefile" ||
filename == "Makefile.in");
}
void performIdleOnlyAsyncRpcMethod(
const core::json::JsonRpcRequest& request,
const core::json::JsonRpcFunctionContinuation& continuation,
const core::json::JsonRpcAsyncFunction& function)
{
if (request.isBackgroundConnection)
{
module_context::scheduleDelayedWork(
boost::posix_time::milliseconds(100),
boost::bind(function, request, continuation),
true);
}
else
{
function(request, continuation);
}
}
} // anonymous namespeace
void scheduleDelayedWork(const boost::posix_time::time_duration& period,
const boost::function &execute,
bool idleOnly)
{
boost::shared_ptr pExecuted(new bool(false));
schedulePeriodicWork(period,
boost::bind(performDelayedWork, execute, pExecuted),
idleOnly,
false);
}
void onBackgroundProcessing(bool isIdle)
{
// allow process supervisor to poll for events
processSupervisor().poll();
// check for file monitor changes
core::system::file_monitor::checkForChanges();
// fire event
events().onBackgroundProcessing(isIdle);
// execute incremental commands
executeScheduledCommands(&s_scheduledCommands);
if (isIdle)
executeScheduledCommands(&s_idleScheduledCommands);
}
#ifdef _WIN32
namespace {
BOOL CALLBACK consoleCtrlHandler(DWORD type)
{
switch (type)
{
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
rstudio::r::exec::setInterruptsPending(true);
return true;
default:
return false;
}
}
} // end anonymous namespace
#endif
void initializeConsoleCtrlHandler()
{
#ifdef _WIN32
// accept Ctrl + C interrupts
::SetConsoleCtrlHandler(nullptr, FALSE);
// remove an old registration (if any)
::SetConsoleCtrlHandler(consoleCtrlHandler, FALSE);
// register console control handler
::SetConsoleCtrlHandler(consoleCtrlHandler, TRUE);
#endif
}
Error registerIdleOnlyAsyncRpcMethod(
const std::string& name,
const core::json::JsonRpcAsyncFunction& function)
{
return registerAsyncRpcMethod(name,
boost::bind(performIdleOnlyAsyncRpcMethod,
_1, _2, function));
}
core::string_utils::LineEnding lineEndings(const core::FilePath& srcFile)
{
// potential special case for Makevars
if (prefs::userPrefs().useNewlinesInMakefiles() && isPackagePosixMakefile(srcFile))
return string_utils::LineEndingPosix;
// get the global default behavior
string_utils::LineEnding lineEndings = prefs::userPrefs().lineEndings();
// use project-level override if available
using namespace session::projects;
ProjectContext& context = projectContext();
if (context.hasProject())
{
if (context.config().lineEndings != r_util::kLineEndingsUseDefault)
lineEndings = (string_utils::LineEnding)context.config().lineEndings;
}
// if we are doing no conversion (passthrough) and there is an existing file
// then we need to peek inside it to see what the existing line endings are
if (lineEndings == string_utils::LineEndingPassthrough)
string_utils::detectLineEndings(srcFile, &lineEndings);
// return computed lineEndings
return lineEndings;
}
Error readAndDecodeFile(const FilePath& filePath,
const std::string& encoding,
bool allowSubstChars,
std::string* pContents)
{
// read contents
std::string encodedContents;
Error error = readStringFromFile(filePath, &encodedContents,
options().sourceLineEnding());
if (error)
return error ;
// convert to UTF-8
return convertToUtf8(encodedContents,
encoding,
allowSubstChars,
pContents);
}
Error convertToUtf8(const std::string& encodedContents,
const std::string& encoding,
bool allowSubstChars,
std::string* pContents)
{
Error error;
error = r::util::iconvstr(encodedContents, encoding, "UTF-8",
allowSubstChars, pContents);
if (error)
return error;
stripBOM(pContents);
// Detect invalid UTF-8 sequences and recover
error = string_utils::utf8Clean(pContents->begin(),
pContents->end(),
'?');
return error ;
}
FilePath userHomePath()
{
return session::options().userHomePath();
}
std::string createAliasedPath(const FileInfo& fileInfo)
{
return createAliasedPath(FilePath(fileInfo.absolutePath()));
}
std::string createAliasedPath(const FilePath& path)
{
return FilePath::createAliasedPath(path, userHomePath());
}
FilePath resolveAliasedPath(const std::string& aliasedPath)
{
return FilePath::resolveAliasedPath(aliasedPath, userHomePath());
}
FilePath userScratchPath()
{
return session::options().userScratchPath();
}
FilePath userUploadedFilesScratchPath()
{
return session::options().userScratchPath().completeChildPath("uploaded-files");
}
FilePath scopedScratchPath()
{
if (projects::projectContext().hasProject())
return projects::projectContext().scratchPath();
else
return userScratchPath();
}
FilePath sharedScratchPath()
{
if (projects::projectContext().hasProject())
return projects::projectContext().sharedScratchPath();
else
return userScratchPath();
}
FilePath sharedProjectScratchPath()
{
if (projects::projectContext().hasProject())
{
return sharedScratchPath();
}
else
{
return FilePath();
}
}
FilePath sessionScratchPath()
{
r_util::ActiveSession& active = activeSession();
if (!active.empty())
return active.scratchPath();
else
return scopedScratchPath();
}
FilePath oldScopedScratchPath()
{
if (projects::projectContext().hasProject())
return projects::projectContext().oldScratchPath();
else
return userScratchPath();
}
std::string rLibsUser()
{
return core::system::getenv("R_LIBS_USER");
}
bool isVisibleUserFile(const FilePath& filePath)
{
return (filePath.isWithin(module_context::userHomePath()) &&
!filePath.isWithin(module_context::userScratchPath()));
}
FilePath safeCurrentPath()
{
return FilePath::safeCurrentPath(userHomePath());
}
FilePath tempFile(const std::string& prefix, const std::string& extension)
{
return r::session::utils::tempFile(prefix, extension);
}
FilePath tempDir()
{
return r::session::utils::tempDir();
}
FilePath findProgram(const std::string& name)
{
std::string which;
Error error = r::exec::RFunction("Sys.which", name).call(&which);
if (error)
{
LOG_ERROR(error);
return FilePath();
}
else
{
return FilePath(which);
}
}
bool addTinytexToPathIfNecessary()
{
// avoid some pathological cases where e.g. TinyTeX folder
// exists but doesn't have the pdflatex binary (don't
// attempt to re-add the folder multiple times)
static bool s_added = false;
if (s_added)
return true;
if (!module_context::findProgram("pdflatex").isEmpty())
return false;
SEXP binDirSEXP;
r::sexp::Protect protect;
Error error = r::exec::RFunction(".rs.tinytexBin").call(&binDirSEXP, &protect);
if (error)
LOG_ERROR(error);
if (!r::sexp::isString(binDirSEXP))
return false;
std::string binDir = r::sexp::asString(binDirSEXP);
FilePath binPath = module_context::resolveAliasedPath(binDir);
if (!binPath.exists())
return false;
s_added = true;
core::system::addToSystemPath(binPath);
return true;
}
bool isPdfLatexInstalled()
{
addTinytexToPathIfNecessary();
return !module_context::findProgram("pdflatex").isEmpty();
}
namespace {
bool hasTextMimeType(const FilePath& filePath)
{
std::string mimeType = filePath.getMimeContentType("");
if (mimeType.empty())
return false;
return boost::algorithm::starts_with(mimeType, "text/") ||
boost::algorithm::ends_with(mimeType, "+xml") ||
boost::algorithm::ends_with(mimeType, "/xml");
}
bool hasBinaryMimeType(const FilePath& filePath)
{
// screen known text types
if (hasTextMimeType(filePath))
return false;
std::string mimeType = filePath.getMimeContentType("");
if (mimeType.empty())
return false;
return boost::algorithm::starts_with(mimeType, "application/") ||
boost::algorithm::starts_with(mimeType, "image/") ||
boost::algorithm::starts_with(mimeType, "audio/") ||
boost::algorithm::starts_with(mimeType, "video/");
}
bool isJsonFile(const FilePath& filePath)
{
std::string mimeType = filePath.getMimeContentType();
return boost::algorithm::ends_with(mimeType, "json");
}
} // anonymous namespace
bool isTextFile(const FilePath& targetPath)
{
if (hasTextMimeType(targetPath))
return true;
if (isJsonFile(targetPath))
return true;
if (hasBinaryMimeType(targetPath))
return false;
if (targetPath.getSize() == 0)
return true;
#ifndef _WIN32
// the behavior of the 'file' command in the macOS High Sierra beta
// changed such that '--mime' no longer ensured that mime-type strings
// were actually emitted. using '-I' instead appears to work around this.
#ifdef __APPLE__
const char * const kMimeTypeArg = "-I";
#else
const char * const kMimeTypeArg = "--mime";
#endif
core::shell_utils::ShellCommand cmd("file");
cmd