/*
 * SessionModuleContext.cpp
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, 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 <atomic>
#include <vector>

#include <fmt/format.h>

#include <boost/assert.hpp>
#include <boost/utility.hpp>
#include <boost/format.hpp>
#include <boost/numeric/conversion/cast.hpp>

#include <shared_core/Error.hpp>
#include <shared_core/FilePath.hpp>
#include <shared_core/Hash.hpp>

#include <core/BoostSignals.hpp>
#include <core/BoostThread.hpp>
#include <core/Thread.hpp>
#include <core/Debug.hpp>
#include <core/FileInfo.hpp>
#include <core/Log.hpp>
#include <core/Base64.hpp>
#include <core/Settings.hpp>
#include <core/DateTime.hpp>
#include <core/FileSerializer.hpp>
#include <core/markdown/Markdown.hpp>
#include <core/system/FileScanner.hpp>
#include <core/IncrementalCommand.hpp>
#include <core/PeriodicCommand.hpp>
#include <core/collection/Tree.hpp>

#include <core/http/Request.hpp>
#include <core/http/Util.hpp>

#include <core/system/Process.hpp>
#include <core/system/FileMonitor.hpp>
#include <core/system/FileChangeEvent.hpp>
#include <core/system/Environment.hpp>
#include <core/system/ShellUtils.hpp>
#include <core/system/Xdg.hpp>

#include <core/r_util/RPackageInfo.hpp>
#include <core/r_util/RActiveSessionsStorage.hpp>

#include <r/RSexp.hpp>
#include <r/RUtil.hpp>
#include <r/RExec.hpp>
#include <r/RRoutines.hpp>
#include <r/RJson.hpp>
#include <r/RJsonRpc.hpp>
#include <r/RSourceManager.hpp>
#include <r/RErrorCategory.hpp>
#include <r/RFunctionHook.hpp>
#include <r/session/RSession.hpp>
#include <r/session/RConsoleActions.hpp>

#include <session/SessionActiveSessionsStorage.hpp>
#include <session/SessionOptions.hpp>
#include <session/SessionPersistentState.hpp>
#include <session/SessionClientEvent.hpp>
#include <session/SessionClientEventService.hpp>

#include <session/http/SessionRequest.hpp>

#include "SessionRpc.hpp"
#include "SessionClientEventQueue.hpp"
#include "SessionMainProcess.hpp"

#include <session/projects/SessionProjects.hpp>

#include <session/SessionConstants.hpp>
#include <session/SessionContentUrls.hpp>
#include <session/SessionQuarto.hpp>

#include <session/prefs/UserPrefs.hpp>
#include <session/prefs/UserState.hpp>

#include <shared_core/system/User.hpp>

#include "modules/SessionBreakpoints.hpp"
#include "modules/SessionFiles.hpp"
#include "modules/SessionReticulate.hpp"
#include "modules/SessionVCS.hpp"

#include "session-config.h"

using namespace rstudio::core;
using namespace boost::placeholders;

namespace rstudio {
namespace session {   
namespace module_context {

bool isSessionSslEnabled()
{
   return !options().getOverlayOption(kSessionSslCertKeyOption).empty();
}

core::Error sendSessionRequest(const std::string& uri,
                               const std::string& body,
                               core::http::Response* pResponse)
{
   return session::http::sendSessionRequest(uri, body, isSessionSslEnabled(), pResponse);
}
      
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()
   {
      // seconds(1) is a real 1-second bound. This replaced a prior bare
      // timed_join(1), which boost interprets as 1 nanosecond -- effectively no
      // wait, which left the thread joinable and could std::terminate from this
      // destructor. The worker polls boost interruption points, so interrupt.
      core::thread::joinOrAbandonThread(
            thread_,
            "console input service thread",
            true,
            boost::posix_time::seconds(1));
   }
   
   void enqueue(const std::string& input)
   {
      requests_.enque(input);
   }
   
private:
   
   void run()
   {
      try
      {
         runImpl();
      }
      catch (boost::thread_interrupted& /* e */)
      {
      }
   }
   
   void runImpl()
   {
      while (true)
      {
         boost::this_thread::interruption_point();
         
         std::string input;
         while (requests_.deque(&input))
         {
            core::http::Response response;
            Error error = session::module_context::sendSessionRequest(
                     "/rpc/console_input",
                     input,
                     &response);
            if (error)
               LOG_ERROR(error);
         }
         
         requests_.wait();
      }
   }
   
   boost::thread thread_;
   core::thread::ThreadsafeQueue<std::string> requests_;
};

ConsoleInputService& consoleInputService()
{
   static ConsoleInputService instance;
   return instance;
}

// enqueClientEvent from R
SEXP rs_enqueClientEvent(SEXP nameSEXP, SEXP dataSEXP)
{
   try
   {
      // ignore forked sessions
      if (main_process::wasForked())
         return R_NilValue;
      
      // 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;
      else if (name == "reticulate_event")
         type = session::client_events::kReticulateEvent;
      else if (name == "environment_assigned")
         type = session::client_events::kEnvironmentAssigned;
      else if (name == "environment_refresh")
         type = session::client_events::kEnvironmentRefresh;
      else if (name == "environment_removed")
         type = session::client_events::kEnvironmentRemoved;
      else if (name == "environment_changed")
         type = session::client_events::kEnvironmentChanged;
      else if (name == "command_callbacks_changed")
         type = session::client_events::kCommandCallbacksChanged;
      else if (name == "console_activate")
         type = session::client_events::kConsoleActivate;

      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;
}

SEXP rs_logInfoMessage(SEXP messageSEXP)
{
   std::string message = r::sexp::asString(messageSEXP);
   LOG_INFO_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 the administrator-configured list of additional functions that may be
// evaluated without prompting when resolving a file preview connection
// expression (see SessionDataPreview.R and the 'preview-allowed-functions'
// session option). Returned as the raw option string; the R side splits it.
SEXP rs_previewAllowedFunctions()
{
   r::sexp::Protect rProtect;
   return r::sexp::create(session::options().previewAllowedFunctions(), &rProtect);
}

// get version
// NOTE: Used by 'rstudioapi'; unofficially part of the API.
SEXP rs_rstudioVersion()
{
   std::string numericVersion(RSTUDIO_VERSION_MAJOR);
   numericVersion.append(".")
      .append(RSTUDIO_VERSION_MINOR).append(".")
      .append(RSTUDIO_VERSION_PATCH).append(".")
      .append(boost::regex_replace(
         std::string(RSTUDIO_VERSION_SUFFIX),
         boost::regex("[a-zA-Z\\-+]"),
         ""));

   r::sexp::Protect rProtect;
   return r::sexp::create(numericVersion, &rProtect);
}

// get long form version
SEXP rs_rstudioLongVersion()
{
   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;
   }
}

// 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)
{
   if (main_process::wasForked())
      return R_NilValue;
   
   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)
{
   if (main_process::wasForked())
      return R_NilValue;
   
   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, SEXP cleanSEXP)
{
   std::string afterRestart = r::sexp::safeAsString(afterRestartSEXP);
   bool clean = r::sexp::asLogical(cleanSEXP);
   
   json::Object dataJson;
   json::Object suspendOptionsJson;
   suspendOptionsJson["save_minimal"] = clean;
   suspendOptionsJson["save_workspace"] = !clean;
   suspendOptionsJson["exclude_packages"] = clean;
   suspendOptionsJson["after_restart"] = afterRestart;
   dataJson["options"] = suspendOptionsJson;
   
   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;
   }
}

SEXP rs_setRpcDelay(SEXP delayMsSEXP)
{
   int delayMs = r::sexp::asInteger(delayMsSEXP);
   rstudio::session::rpc::setRpcDelay(delayMs);
   return delayMsSEXP;
}

// Force background processing to occur; used during testing since the user is never idle during
// headless test execution
SEXP rs_performBackgroundProcessing(SEXP isIdle)
{
   onBackgroundProcessing(r::sexp::asLogical(isIdle));
   return R_NilValue;
}

} // anonymous namespace


// register a scratch path which is monitored

namespace {

typedef std::map<FilePath,OnFileChange> 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<core::system::FileChangeEvent>& 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<tree<FileInfo> > monitoredPathTree(
   Error* pScanError = nullptr)
{
   boost::shared_ptr<tree<FileInfo> > pMonitoredTree(new tree<FileInfo>());
   core::system::FileScannerOptions options;
   options.recursive = true;
   options.filter = monitoredScratchFilter;
   Error scanError = scanFiles(FileInfo(monitoredParentPath()),
                               options,
                               pMonitoredTree.get());
   if (scanError)
      LOG_ERROR(scanError);

   if (pScanError)
      *pScanError = scanError;

   return pMonitoredTree;
}

bool scanForMonitoredPathChanges(boost::shared_ptr<tree<FileInfo> > pPrevTree)
{
   // scan the current state of the monitored directory
   Error scanError;
   boost::shared_ptr<tree<FileInfo> > pCurrentTree =
      monitoredPathTree(&scanError);

   // if the scan failed, skip this cycle and retry next interval;
   // keep the previous (good) tree so we don't generate spurious events
   if (scanError)
      return true;

   // check for changes
   std::vector<core::system::FileChangeEvent> changes;
   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()
{
#ifdef _WIN32
   // Use periodic scanning instead of native file monitoring for the
   // monitored scratch directory. This directory is tiny (a handful of
   // files, a few KB) so scanning is cheap, and it avoids
   // ReadDirectoryChangesW / IRP_MJ_DIRECTORY_CONTROL operations that
   // enterprise endpoint security software can intercept synchronously,
   // adding seconds to startup time on Windows.
   s_monitorByScanning = true;
   module_context::schedulePeriodicWork(
      boost::posix_time::seconds(3),
      boost::bind(scanForMonitoredPathChanges, monitoredPathTree()),
      true);
#else
   // 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);
#endif
}


} // 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<void(const r::session::RSuspendOptions&,Settings*),
                 RSTUDIO_BOOST_LAST_VALUE<void>,
                 int,
                 std::less<int> > suspendSignal_;
                  
   RSTUDIO_BOOST_SIGNAL<void(const Settings&),
                 RSTUDIO_BOOST_LAST_VALUE<void>,
                 int,
                 std::greater<int> > 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<boost::shared_ptr<ScheduledCommand> >
                                                      ScheduledCommands;
ScheduledCommands s_scheduledCommands;
ScheduledCommands s_idleScheduledCommands;

void addScheduledCommand(boost::shared_ptr<ScheduledCommand> 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<bool()>& execute,
         bool idleOnly)
{
   addScheduledCommand(boost::shared_ptr<ScheduledCommand>(
                           new IncrementalCommand(incrementalDuration,
                                                  execute)),
                         idleOnly);
}

void scheduleIncrementalWork(
         const boost::posix_time::time_duration& initialDuration,
         const boost::posix_time::time_duration& incrementalDuration,
         const boost::function<bool()>& execute,
         bool idleOnly)
{
   addScheduledCommand(boost::shared_ptr<ScheduledCommand>(
                           new IncrementalCommand(initialDuration,
                                                  incrementalDuration,
                                                  execute)),
                           idleOnly);
}


void schedulePeriodicWork(const boost::posix_time::time_duration& period,
                          const boost::function<bool()> &execute,
                          bool idleOnly,
                          bool immediate)
{
   addScheduledCommand(boost::shared_ptr<ScheduledCommand>(
                           new PeriodicCommand(period, execute, immediate)),
                       idleOnly);
}


namespace {

bool performDelayedWork(const boost::function<void()> &execute,
                        boost::shared_ptr<bool> 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<void()> &execute,
                         bool idleOnly)
{
   boost::shared_ptr<bool> pExecuted(new bool(false));

   schedulePeriodicWork(period,
                        boost::bind(performDelayedWork, execute, pExecuted),
                        idleOnly,
                        false);
}

void executeOnMainThread(const boost::function<void()> &execute)
{
   if (core::thread::isMainThread())
      execute();
   else
      scheduleDelayedWork(boost::posix_time::milliseconds(1), execute, 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()));
}

bool reduceRemoteFilesystemOperations()
{
   // resolved per-project (combines the project setting, the global
   // preference, and remote-filesystem detection); no project means no
   // reduction
   if (!projects::projectContext().hasProject())
      return false;

   return projects::projectContext().reduceRemoteFilesystemOperations();
}

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)
{
   // NOTE: Previously, we checked both 'system::findProgramOnPath()' as well
   // as R's 'Sys.which()'. We prefer using 'findProgramOnPath()' here as it
   // is threadsafe, and there are some contexts where this function might be
   // invoked on a background thread, e.g. when called via the offlineService()
   // thread.
   //
   // The main concern is Windows, where it's possible that the rsession and
   // embedded R process could be referencing separate 'environ' blocks,
   // and so the PATHs used for finding programs may be different. However,
   // ~1 year of testing didn't identify any real issues so we just use the
   // thread-safe version now.
   FilePath resultPath;
   Error error = system::findProgramOnPath(name, &resultPath);
   if (error)
      return FilePath();

   return resultPath;
}

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_NilValue;
   r::sexp::Protect protect;
   Error error = r::exec::RFunction(".rs.tinytexBin").call(&binDirSEXP, &protect);
   if (error)
   {
      LOG_ERROR(error);
      return false;
   }
   
   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
   
   std::string fileCommand = "file";
   
   // 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";
   if (FilePath::exists("/usr/bin/file"))
      fileCommand = "/usr/bin/file";
#else
   const char * const kMimeTypeArg = "--mime";
#endif
   
   core::shell_utils::ShellCommand cmd(fileCommand);
   cmd << "--dereference";
   cmd << kMimeTypeArg;
   cmd << "--brief";
   cmd << targetPath;
   core::system::ProcessResult result;
   Error error = core::system::runCommand(cmd,
                                          core::system::ProcessOptions(),
                                          &result);
   if (error)
   {
      LOG_ERROR(error);
      return !!error;
   }

   // strip encoding
   std::string fileType = boost::algorithm::trim_copy(result.stdOut);
   fileType = fileType.substr(0, fileType.find(';'));

   // check value
   return boost::algorithm::starts_with(fileType, "text/") ||
          boost::algorithm::ends_with(fileType, "+xml") ||
          boost::algorithm::ends_with(fileType, "/xml") ||
          boost::algorithm::ends_with(fileType, "x-empty") ||
          boost::algorithm::equals(fileType, "application/json") ||
          boost::algorithm::equals(fileType, "application/postscript");
#else

   // read contents of file
   std::string contents;
   Error error = core::readStringFromFile(targetPath, &contents);
   if (error)
   {
      LOG_ERROR(error);
      return !!error;
   }

   // does it have null bytes?
   std::string nullBytes;
   nullBytes.push_back('\0');
   nullBytes.push_back('\0');
   return !boost::algorithm::contains(contents, nullBytes);

#endif

}

void editFile(const core::FilePath& filePath, int lineNumber)
{
   // construct file system item (also tag with mime type) and position
   json::Object fileJson = module_context::createFileSystemItem(filePath);
   fileJson["mime_type"] = filePath.getMimeContentType();

   json::Value positionJsonValue;
   if (lineNumber >= 0)
   {
      json::Object positionJson;
      positionJson["line"] = lineNumber;
      positionJson["column"] = 1;
      positionJsonValue = positionJson;
   }

   // fire event
   json::Object eventJson;
   eventJson["file"] = fileJson;
   eventJson["position"] = positionJsonValue;
   ClientEvent event(client_events::kFileEdit, eventJson);
   module_context::enqueClientEvent(event);
}

Error rBinDir(core::FilePath* pRBinDirPath)
{
   std::string rHomeBin;
   r::exec::RFunction rHomeBinFunc("R.home", "bin");
   Error error = rHomeBinFunc.call(&rHomeBin);
   if (error)
      return error;

   *pRBinDirPath = FilePath(rHomeBin);
   return Success();
}


Error rScriptPath(FilePath* pRScriptPath)
{
   FilePath rHomeBinPath;
   Error error = rBinDir(&rHomeBinPath);
   if (error)
      return error;

#ifdef _WIN32
   *pRScriptPath = rHomeBinPath.completePath("Rterm.exe");
#else
   *pRScriptPath = rHomeBinPath.completePath("R");
#endif
   
   return Success();
}

shell_utils::ShellCommand rCmd(const core::FilePath& rBinDir)
{
#ifdef _WIN32
      return shell_utils::ShellCommand(rBinDir.completeChildPath("Rcmd.exe"));
#else
      shell_utils::ShellCommand rCmd(rBinDir.completeChildPath("R"));
      rCmd << "CMD";
      return rCmd;
#endif
}

// get the R local help port
std::string rLocalHelpPort()
{
   std::string port;
   Error error = r::exec::RFunction(".rs.httpdPort").call(&port);
   if (error)
      LOG_ERROR(error);
   return port;
}

std::vector<FilePath> getLibPaths()
{
   std::vector<std::string> libPathsString;
   r::exec::RFunction rfLibPaths(".libPaths");
   Error error = rfLibPaths.call(&libPathsString);
   if (error)
      LOG_ERROR(error);

   std::vector<FilePath> libPaths;
   for (const std::string& path : libPathsString)
   {
      libPaths.push_back(module_context::resolveAliasedPath(path));
   }

   return libPaths;
}

bool restoreWorkspaceEnabled()
{
   // session-level override (e.g. from rsession.conf or command line)
   if (options().rRestoreWorkspace() == kRestoreWorkspaceNo)
      return false;
   else if (options().rRestoreWorkspace() == kRestoreWorkspaceYes)
      return true;

   // project-level setting
   const projects::ProjectContext& projContext = projects::projectContext();
   if (projContext.hasProject())
   {
      switch (projContext.config().restoreWorkspace)
      {
      case r_util::YesValue:
         return true;
      case r_util::NoValue:
         return false;
      default:
         break;
      }
   }

   // fall back to user pref or environment file override
   return prefs::userPrefs().loadWorkspace() ||
          !options().initialEnvironmentFileOverride().isEmpty();
}

bool disablePackages()
{
   return !core::system::getenv("RSTUDIO_DISABLE_PACKAGES").empty();
}

bool isPositAssistantEnabledByAdmin()
{
   return session::options().allowPositAssistant() &&
          session::options().positAssistantEnabled() &&
          core::system::getenv("RSTUDIO_DISABLE_POSIT_ASSISTANT").empty();
}

bool isPositAssistantEnabled()
{
   return isPositAssistantEnabledByAdmin() &&
          (prefs::userPrefs().assistant() != kAssistantNone ||
           prefs::userPrefs().chatProvider() != kChatProviderNone);
}

// check if a package is installed
bool isPackageInstalled(const std::string& packageName)
{
   r::session::utils::SuppressOutputInScope suppressOutput;

   bool installed = false;
   r::exec::RFunction func(".rs.isPackageInstalled", packageName);
   Error error = func.call(&installed);
   return !error ? installed : false;
}

bool isPackageVersionInstalled(const std::string& packageName,
                               const std::string& version)
{
   r::session::utils::SuppressOutputInScope suppressOutput;

   bool installed = false;
   r::exec::RFunction func(".rs.isPackageVersionInstalled",
                           packageName, version);
   Error error = func.call(&installed);
   return !error ? installed : false;
}

bool isMinimumDevtoolsInstalled()
{
   return isPackageVersionInstalled("devtools", "1.4.1");
}

bool isMinimumRoxygenInstalled()
{
   return isPackageVersionInstalled("roxygen2", "4.0");
}

std::string packageVersion(const std::string& packageName)
{
   std::string version;
   Error error = r::exec::RFunction(".rs.packageVersionString")
         .addParam(packageName)
         .call(&version);
   
   if (error)
   {
      LOG_ERROR(error);
      return "(Unknown)";
   }
   else
   {
      return version;
   }
}

Error packageVersion(const std::string& packageName,
                     core::Version* pVersion)
{
   std::string version;
   Error error = r::exec::RFunction(".rs.packageVersionString")
         .addParam(packageName)
         .call(&version);
   
   if (error)
      return error;
   
   *pVersion = Version(version);
   return Success();
}

bool hasMinimumRVersion(const std::string& version)
{
   bool hasVersion = false;
   boost::format fmt("getRversion() >= '%1%'");
   std::string versionTest = boost::str(fmt % version);
   Error error = r::exec::evaluateString(versionTest, &hasVersion);
   if (error)
      LOG_ERROR(error);
   return hasVersion;
}

PackageCompatStatus getPackageCompatStatus(
      const std::string& packageName,
      const std::string& packageVersion,
      int protocolVersion)
{
   r::session::utils::SuppressOutputInScope suppressOutput;
   int compatStatus = COMPAT_UNKNOWN;
   r::exec::RFunction func(".rs.getPackageCompatStatus",
                           packageName, packageVersion, protocolVersion);
   Error error = func.call(&compatStatus);
   if (error)
   {
      LOG_ERROR(error);
      return COMPAT_UNKNOWN;
   }
   return static_cast<PackageCompatStatus>(compatStatus);
}

Error installPackage(const std::string& pkgPath, const std::string& libPath)
{
   // get R bin directory
   FilePath rBinDir;
   Error error = module_context::rBinDir(&rBinDir);
   if (error)
      return error;

   // setup options and command
   core::system::ProcessOptions options;
#ifdef _WIN32
   shell_utils::ShellCommand installCommand(rBinDir.completeChildPath("R.exe"));
#else
   shell_utils::ShellCommand installCommand(rBinDir.completeChildPath("R"));
#endif

   installCommand << core::shell_utils::EscapeFilesOnly;

   // for packrat projects we execute the profile and set the working
   // directory to the project directory; for other contexts we just
   // propagate the R_LIBS
   if (module_context::packratContext().modeOn)
   {
      options.workingDir = projects::projectContext().directory();
   }
   else
   {
      installCommand << "--vanilla";
      core::system::Options env;
      core::system::environment(&env);
      std::string libPaths = libPathsString();
      if (!libPaths.empty())
         core::system::setenv(&env, "R_LIBS", libPathsString());
      options.environment = env;
   }

   installCommand << "CMD" << "INSTALL";

   // if there is a lib path then provide it
   if (!libPath.empty())
   {
      installCommand << "-l";
      installCommand << "\"" + libPath + "\"";
   }

   // add package path
   installCommand << "\"" + pkgPath + "\"";
   core::system::ProcessResult result;

   // run the command
   error = core::system::runCommand(installCommand,
                                    options,
                                    &result);

   if (error)
      return error;

   if ((result.exitStatus != EXIT_SUCCESS) && !result.stdErr.empty())
   {
      return systemError(boost::system::errc::state_not_recoverable,
                         "Error installing package: " + result.stdErr,
                         ERROR_LOCATION);
   }

   return Success();
}


std::string packageNameForSourceFile(const core::FilePath& sourceFilePath)
{
   // check whether we are in a package
   FilePath sourceDir = sourceFilePath.getParent();
   if (sourceDir.getFilename() == "R" &&
       r_util::isPackageDirectory(sourceDir.getParent()))
   {
      r_util::RPackageInfo pkgInfo;
      Error error = pkgInfo.read(sourceDir.getParent());
      if (error)
      {
         LOG_ERROR(error);
         return std::string();
      }

      return pkgInfo.name();
   }
   else
   {
      return std::string();
   }
}

bool isUnmonitoredPackageSourceFile(const FilePath& filePath)
{
   // if it's in the current package then it's fine
   using namespace projects;
   if (projectContext().hasProject() &&
      (projectContext().config().buildType == r_util::kBuildTypePackage) &&
       filePath.isWithin(projectContext().buildTargetPath()))
   {
      return false;
   }

   // ensure we are dealing with a directory
   FilePath dir = filePath;
   if (!dir.isDirectory())
      dir = filePath.getParent();

   // see if one the file's parent directories has a DESCRIPTION
   while (!dir.isEmpty())
   {
      FilePath descPath = dir.completeChildPath("DESCRIPTION");
      if (descPath.exists())
      {
         // get path relative to package dir
         std::string relative = filePath.getRelativePath(dir);
         if (boost::algorithm::starts_with(relative, "R/") ||
             boost::algorithm::starts_with(relative, "src/") ||
             boost::algorithm::starts_with(relative, "inst/include/"))
         {
            return true;
         }
         else
         {
            return false;
         }
      }

      dir = dir.getParent();
   }

   return false;
}


SEXP rs_packageNameForSourceFile(SEXP sourceFilePathSEXP)
{
   r::sexp::Protect protect;
   FilePath sourceFilePath = module_context::resolveAliasedPath(r::sexp::asString(sourceFilePathSEXP));
   return r::sexp::create(packageNameForSourceFile(sourceFilePath), &protect);
}

SEXP rs_base64encode(SEXP dataSEXP, SEXP binarySEXP)
{
   bool binary = r::sexp::asLogical(binarySEXP);
   const char* pData;
   std::size_t n;

   if (TYPEOF(dataSEXP) == STRSXP)
   {
      SEXP charSEXP = STRING_ELT(dataSEXP, 0);
      pData = CHAR(charSEXP);
      n = r::sexp::length(charSEXP);
   }
   else if (TYPEOF(dataSEXP) == RAWSXP)
   {
      pData = reinterpret_cast<const char*>(RAW(dataSEXP));
      n = r::sexp::length(dataSEXP);
   }
   else
   {
      LOG_ERROR_MESSAGE("Unexpected data type");
      return R_NilValue;
   }

   std::string output;
   Error error = base64::encode(pData, n, &output);
   if (error)
      LOG_ERROR(error);

   r::sexp::Protect protect;
   if (binary)
      return r::sexp::createRawVector(output, &protect);
   else
      return r::sexp::create(output, &protect);
}

SEXP rs_base64encodeFile(SEXP pathSEXP)
{
   std::string path = r::sexp::asString(pathSEXP);
   FilePath filePath = module_context::resolveAliasedPath(path);

   std::string output;
   Error error = base64::encode(filePath, &output);
   if (error)
      LOG_ERROR(error);

   r::sexp::Protect protect;
   return r::sexp::create(output, &protect);
}

SEXP rs_base64decode(SEXP dataSEXP, SEXP binarySEXP)
{
   bool binary = r::sexp::asLogical(binarySEXP);
   const char* pData;
   std::size_t n;

   if (TYPEOF(dataSEXP) == STRSXP)
   {
      SEXP charSEXP = STRING_ELT(dataSEXP, 0);
      pData = CHAR(charSEXP);
      n = r::sexp::length(charSEXP);
   }
   else if (TYPEOF(dataSEXP) == RAWSXP)
   {
      pData = reinterpret_cast<const char*>(RAW(dataSEXP));
      n = r::sexp::length(dataSEXP);
   }
   else
   {
      LOG_ERROR_MESSAGE("Unexpected data type");
      return R_NilValue;
   }

   std::string output;
   Error error = base64::decode(pData, n, &output);
   if (error)
      LOG_ERROR(error);

   r::sexp::Protect protect;
   if (binary)
      return r::sexp::createRawVector(output, &protect);
   else
      return r::sexp::create(output, &protect);
}

SEXP rs_htmlEscape(SEXP textSEXP, SEXP attributeSEXP)
{
   std::string escaped = string_utils::htmlEscape(r::sexp::safeAsString(textSEXP), 
         r::sexp::asLogical(attributeSEXP));
   r::sexp::Protect protect;
   return r::sexp::create(escaped, &protect);
}

SEXP rs_resolveAliasedPath(SEXP pathSEXP)
{
   std::string path = r::sexp::asUtf8String(pathSEXP);
   FilePath resolved = module_context::resolveAliasedPath(path);
   r::sexp::Protect protect;
   return r::sexp::create(resolved.getAbsolutePath(), &protect);
}

SEXP rs_sessionModulePath()
{
   r::sexp::Protect protect;
   return r::sexp::create(
      session::options().modulesRSourcePath().getAbsolutePath(), &protect);
}

json::Object createFileSystemItem(const FileInfo& fileInfo)
{
   json::Object entry;

   std::string aliasedPath = module_context::createAliasedPath(fileInfo);
   std::string rawPath =
      module_context::resolveAliasedPath(aliasedPath).getAbsolutePath();

   entry["path"] = aliasedPath;
   if (aliasedPath != rawPath)
      entry["raw_path"] = rawPath;
   entry["dir"] = fileInfo.isDirectory();

   // length requires cast
   try
   {
      entry["length"] = boost::numeric_cast<boost::uint64_t>(fileInfo.size());
   }
   catch (const boost::bad_numeric_cast& e)
   {
      LOG_ERROR_MESSAGE(std::string("Error converting file size: ") +
                        e.what());
      entry["length"] = 0;
   }
   
   entry["exists"] = FilePath(fileInfo.absolutePath()).exists();

   entry["lastModified"] = date_time::millisecondsSinceEpoch(
                                                   fileInfo.lastWriteTime());
   return entry;
}

json::Object createFileSystemItem(const FilePath& filePath)
{
   return createFileSystemItem(FileInfo(filePath));
}

std::string rVersion()
{
   std::string rVersion;
   Error error = rstudio::r::exec::RFunction(".rs.rVersionString")
         .call(&rVersion);
   if (error)
      LOG_ERROR(error);

   return rVersion;
}

std::string rVersionLabel()
{
   std::string versionLabel = system::getenv("RSTUDIO_R_VERSION_LABEL");
   return versionLabel;
}

std::string rHomeDir()
{
   // get the current R home directory
   std::string rVersionHome;
   Error error = rstudio::r::exec::RFunction("R.home").call(&rVersionHome);
   if (error)
      LOG_ERROR(error);
   return rVersionHome;
}

std::string rVersionModule()
{
   std::string module = system::getenv("RSTUDIO_R_MODULE");
   return module;
}

r_util::ActiveSession& activeSession()
{
   static boost::shared_ptr<r_util::ActiveSession> pSession;
   if (!pSession)
   {
      std::string id = options().sessionScope().id();

      if (!id.empty())
         pSession = activeSessions().get(id);
      else if (options().programMode() == kSessionProgramModeDesktop)
      {
         // if no active session, create one and use the launcher token as a
         // synthetic session ID
         //
         // we only do this in desktop mode to preserve backwards compatibility
         // with some functionality that depends on session data
         // persisting after rstudio has been closed
         //
         // this entire clause will likely need to be reverted in a future release
         // once we ensure that all execution modes have this squared away
         pSession = activeSessions().emptySession(options().launcherToken());
      }
      else
      {
         // if no scope was specified, we are in singleton session mode
         // check to see if there is an existing active session, and use that
         std::vector<boost::shared_ptr<r_util::ActiveSession> > sessions =
               activeSessions().list(true);
         if (sessions.size() > 0)
         {
            // there is more than one session but no session id was passed in. This is OS server or pro with server-multiple-sessions=0
            // so we must be referring to the most recent session.
            pSession = sessions.front();

            if (sessions.size() == 1)
               LOG_DEBUG_MESSAGE("Reusing singleton session: " + pSession->id());
            else
               LOG_DEBUG_MESSAGE("Reusing most recent session: " + pSession->id() + " out of: " + std::to_string(sessions.size()));
         }
         else
         {

            // create a new session entry
            // this should not really run because in single session mode there will
            // only be one session but we'll create one here just in case for safety
            std::string sessionId;

            // pass ~ as the default working directory
            // this is resolved deeper in the code in SessionClientInit to turn into
            // the actual user preference or session default directory that it should be
            activeSessions().create(options().sessionScope().project(), "~", &sessionId);
            pSession = activeSessions().get(sessionId);

            LOG_DEBUG_MESSAGE("Creating new session in singleton session mode: " + sessionId);
         }
      }
   }
   return *pSession;
}


r_util::ActiveSessions& activeSessions()
{
   static boost::shared_ptr<r_util::ActiveSessions> pSessions;
   if (!pSessions)
   {
      std::shared_ptr<r_util::IActiveSessionsStorage> storage;
      Error error = storage::activeSessionsStorage(&storage);

      // The only real error we can get here is if the current user can't 
      // be retrieved, but if that's the case we should have exited with a 
      // failure during start-up. We'll probably SegFault in any calls to the
      // ActiveSession object, but the process is in a very broken state anyway
      // Log before we crash so we can know what went wrong
      if (error)
         LOG_ERROR(error);

      pSessions.reset(new r_util::ActiveSessions(storage, userScratchPath()));
   }
   
   return *pSessions;
}



std::string libPathsString()
{
   // call into R to get the string
   std::string libPaths;
   Error error = r::exec::RFunction(".rs.libPathsString").call(&libPaths);
   if (error)
   {
      LOG_ERROR(error);
      return std::string();
   }

   // this is presumably system-encoded, so convert this to utf8 before return
   return string_utils::systemToUtf8(libPaths);
}

Error sourceModuleRFile(const std::string& rSourceFile)
{
   FilePath modulesPath = session::options().modulesRSourcePath();
   FilePath srcPath = modulesPath.completePath(rSourceFile);
   return r::sourceManager().sourceTools(srcPath);
}

Error sourceModuleRFileWithResult(const std::string& rSourceFile,
                                  const FilePath& workingDir,
                                  core::system::ProcessResult* pResult)
{
   // R binary
   FilePath rProgramPath;
   Error error = module_context::rScriptPath(&rProgramPath);
   if (error)
      return error;
   std::string rBin = string_utils::utf8ToSystem(rProgramPath.getAbsolutePath());

   // vanilla execution of a single expression
   std::vector<std::string> args;
   args.push_back("--vanilla");
   args.push_back("-s");
   args.push_back("-e");

   // build source command
   boost::format fmt("source('%1%')");
   FilePath modulesPath = session::options().modulesRSourcePath();
   FilePath srcFilePath = modulesPath.completePath(rSourceFile);
   std::string srcPath = core::string_utils::utf8ToSystem(
      srcFilePath.getAbsolutePath());
   std::string escapedSrcPath = string_utils::jsLiteralEscape(srcPath);
   std::string cmd = boost::str(fmt % escapedSrcPath);
   args.push_back(cmd);

   // options
   core::system::ProcessOptions options;
   options.terminateChildren = true;
   options.workingDir = workingDir;

   // allow child process to inherit our R_LIBS
   core::system::Options childEnv;
   core::system::environment(&childEnv);
   std::string libPaths = libPathsString();
   if (!libPaths.empty())
      core::system::setenv(&childEnv, "R_LIBS", libPaths);
   options.environment = childEnv;

   // run the child
   return core::system::runProgram(rBin, args, "", options, pResult);
}

      
void enqueClientEvent(const ClientEvent& event)
{
   session::clientEventQueue().add(event);
}

bool isDirectoryMonitored(const FilePath& directory)
{
   return session::projects::projectContext().isMonitoringDirectory(directory) ||
          session::modules::files::isMonitoringDirectory(directory);
}

bool isRScriptInPackageBuildTarget(const FilePath &filePath)
{
   using namespace session::projects;

   if (projectContext().config().buildType == r_util::kBuildTypePackage)
   {
      FilePath pkgPath = projects::projectContext().buildTargetPath();
      std::string pkgRelative = filePath.getRelativePath(pkgPath);
      return boost::algorithm::starts_with(pkgRelative, "R/");
   }
   else
   {
      return false;
   }
}

SEXP rs_isRScriptInPackageBuildTarget(SEXP filePathSEXP)
{
   r::sexp::Protect protect;
   FilePath filePath = module_context::resolveAliasedPath(
      r::sexp::asUtf8String(filePathSEXP));
   return r::sexp::create(isRScriptInPackageBuildTarget(filePath), &protect);
}

bool fileListingFilter(const core::FileInfo& fileInfo, bool hideObjectFiles)
{
   core::FilePath filePath(fileInfo.absolutePath());

   // Check to see if this is one of the extensions we always show
   std::string ext = filePath.getExtensionLowerCase();
   core::json::Array exts = prefs::userPrefs().alwaysShownExtensions();
   for (json::Value val : exts)
   {
      if (json::isType<std::string>(val))
      {
         if (ext == string_utils::toLower(val.getString()))
         {
            return true;
         }
      }
   }

   // Check to see if this is one of the files we always show
   std::string name = filePath.getFilename();
   core::json::Array files = prefs::userPrefs().alwaysShownFiles();
   for (json::Value val : files)
   {
      if (json::isType<std::string>(val))
      {
         if (name == val.getString())
         {
            return true;
         }
      }
   }
   
   // Check for hidden files
   if (filePath.isHidden())
      return false;
   
   // Check for object files
   if (hideObjectFiles && (ext == ".o" || ext == ".so" || ext == ".dll"))
      return false;
   
   // ok, passed our filters
   return true;
}

namespace {

// enque file changed event
void enqueFileChangedEvent(
      const core::system::FileChangeEvent& event,
      boost::shared_ptr<modules::source_control::FileDecorationContext> pCtx)
{
   // create file change object
   json::Object fileChange;
   fileChange["type"] = event.type();
   json::Object fileSystemItem = createFileSystemItem(event.fileInfo());

   if (prefs::userPrefs().vcsAutorefresh())
   {
      pCtx->decorateFile(
               FilePath(event.fileInfo().absolutePath()),
               &fileSystemItem);
   }

   fileChange["file"] = fileSystemItem;

   // enque it
   ClientEvent clientEvent(client_events::kFileChanged, fileChange);
   module_context::enqueClientEvent(clientEvent);
}

} // namespace

void enqueFileChangedEvent(const core::system::FileChangeEvent &event)
{
   FilePath filePath = FilePath(event.fileInfo().absolutePath());

   using namespace session::modules::source_control;
   auto pCtx = fileDecorationContext(filePath, true);
   enqueFileChangedEvent(event, pCtx);
}

void enqueFileChangedEvents(const core::FilePath& vcsStatusRoot,
                            const std::vector<core::system::FileChangeEvent>& events)
{
   using namespace modules::source_control;

   if (events.empty())
      return;

   // try to find the common parent of the events
   FilePath commonParentPath = FilePath(events.front().fileInfo().absolutePath()).getParent();
   for (const core::system::FileChangeEvent& event : events)
   {
      // if not within the common parent then revert to the vcs status root
      if (!FilePath(event.fileInfo().absolutePath()).isWithin(commonParentPath))
      {
         commonParentPath = vcsStatusRoot;
         break;
      }
   }

   using namespace session::modules::source_control;
   auto pCtx = fileDecorationContext(commonParentPath, true);

   // fire client events as necessary
   for (const core::system::FileChangeEvent& event : events)
   {
      enqueFileChangedEvent(event, pCtx);
   }
}

Error enqueueConsoleInput(const std::string& consoleInput)
{
   using namespace r::session;
   
   // construct our JSON RPC
   json::Array jsonParams = RConsoleInput(consoleInput).toJsonArray();
   
   json::Object jsonRpc;
   jsonRpc["method"] = "console_input";
   jsonRpc["params"] = jsonParams;
   jsonRpc["clientId"] = clientEventService().clientId();
   
   // serialize for transmission
   std::ostringstream oss;
   jsonRpc.write(oss);
   
   // and fire it off
   consoleInputService().enqueue(oss.str());
   
   return Success();
}

// NOTE: we used to call explicitly back into r::session to write output
// and errors however the fact that these functions are called from
// background threads during std stream capture means that they must
// provide thread safety guarantees. since the r::session module generally
// assumes single-threaded operation it is more straightforward to have
// the code here ape the actions of RWriteConsoleEx with a more explicit
// thread safety constraint

namespace {
std::atomic<bool> s_agentExecuting{false};
std::atomic<bool> s_consoleOutputSuppressed{false};
} // anonymous namespace

void setAgentExecuting(bool executing)
{
   s_agentExecuting.store(executing, std::memory_order_release);
}

bool isAgentExecuting()
{
   return s_agentExecuting.load(std::memory_order_acquire);
}

void setConsoleOutputSuppressed(bool suppressed)
{
   s_consoleOutputSuppressed.store(suppressed, std::memory_order_release);
}

bool isConsoleOutputSuppressed()
{
   return s_consoleOutputSuppressed.load(std::memory_order_acquire);
}

void consoleWriteOutput(const std::string& output)
{
   // NOTE: all actions herein must be threadsafe! (see comment above)

   if (s_agentExecuting)
   {
      // Send as a JSON object so the agent flag survives the event queue's
      // string-based output buffering, and the frontend can style it.
      json::Object data;
      data["text"]    = output;
      data["console"] = std::string();
      data["agent"]   = true;
      ClientEvent event(client_events::kConsoleWriteOutput, data);
      enqueClientEvent(event);
   }
   else
   {
      // enque write output (same as session::rConsoleWrite)
      ClientEvent event(client_events::kConsoleWriteOutput, output);
      enqueClientEvent(event);
   }
}

void consoleWriteError(const std::string& message)
{
   // NOTE: all actions herein must be threadsafe! (see comment above)

   if (s_agentExecuting)
   {
      json::Object data;
      data["text"]    = message;
      data["console"] = std::string();
      data["agent"]   = true;
      ClientEvent event(client_events::kConsoleWriteError, data);
      enqueClientEvent(event);
   }
   else
   {
      // enque write error (same as session::rConsoleWrite)
      ClientEvent event(client_events::kConsoleWriteError, message);
      enqueClientEvent(event);
   }
}

void showErrorMessage(const std::string& title, const std::string& message)
{
   session::clientEventQueue().add(showErrorMessageEvent(title, message));
}

void showMessage(int type, const std::string& title, const std::string& message)
{
   session::clientEventQueue().add(showMessageEvent(type, title, message));
}

void showFile(const FilePath& filePath, const std::string& window)
{
   if (session::options().programMode() == kSessionProgramModeDesktop)
   {
      // for pdfs handle specially for each platform
      if (filePath.getExtensionLowerCase() == ".pdf")
      {
         std::string path = filePath.getAbsolutePath();
         Error error = r::exec::RFunction(".rs.shellViewPdf", path).call();
         if (error)
            LOG_ERROR(error);
      }
      else
      {
         ClientEvent event = browseUrlEvent("file:///" + filePath.getAbsolutePath());
         module_context::enqueClientEvent(event);
      }
   }
   else if (session::options().programMode() == kSessionProgramModeServer)
   {
      if (!isPathViewAllowed(filePath))
      {
         module_context::showErrorMessage(
            "File Download Error",
            "This system administrator has not granted you permission "
            "to view this file.\n");
      }
      else if (session::options().allowFileDownloads())
      {
         std::string url = createFileUrl(filePath);
         ClientEvent event = browseUrlEvent(url);
         module_context::enqueClientEvent(event);
      }
      else
      {
         module_context::showErrorMessage(
            "File Download Error",
            "Unable to show file because file downloads are restricted "
            "on this server.\n");
      }
   }
}

bool shouldAuditFileDownload(const core::http::Request& request,
                             const core::FilePath& filePath)
{
   // Skip when the URL was constructed by an internal preview/show flow
   // (?show=1 marker, set by createFileUrl above and by Files.java's
   // companion showFileInBrowser for view-style navigations).
   if (request.queryParamValue("show") == "1")
   {
      LOG_DEBUG_MESSAGE("Audit skipped (?show=1 marker): " +
                        filePath.getAbsolutePath());
      return false;
   }

   // Skip HTML sub-resource fetches. Browsers signal these via
   // Sec-Fetch-Dest. Top-level navigations send "document" (or omit the
   // header on older browsers); embedded resources send their resource
   // type. Treat anything in the explicit sub-resource set as not-a-
   // download so a single HTML preview doesn't generate an audit row
   // per asset. "iframe" and "frame" are deliberately NOT in this set:
   // an iframe loading a binary is effectively exfiltration, so let
   // the Content-Type check below decide.
   const std::string fetchDest = request.headerValue("Sec-Fetch-Dest");
   if (fetchDest == "style"        || fetchDest == "script"        ||
       fetchDest == "image"        || fetchDest == "font"          ||
       fetchDest == "audio"        || fetchDest == "video"         ||
       fetchDest == "track"        || fetchDest == "object"        ||
       fetchDest == "embed"        || fetchDest == "manifest"      ||
       fetchDest == "xslt"         || fetchDest == "report"        ||
       fetchDest == "worker"       || fetchDest == "serviceworker" ||
       fetchDest == "audioworklet" || fetchDest == "paintworklet")
   {
      LOG_DEBUG_MESSAGE("Audit skipped (Sec-Fetch-Dest=" + fetchDest +
                        " sub-resource): " + filePath.getAbsolutePath());
      return false;
   }

   // Skip when the response will be served as a browser-renderable
   // Content-Type. The bytes are being viewed inline rather than saved
   // to disk, so it isn't a download from the user's perspective. Note
   // this means a real "right-click -> Save As" on a rendered PDF will
   // not produce an audit row; that's an accepted trade-off for not
   // logging the much more common inline-view case.
   //
   // For the audit decision we pass "application/octet-stream" as the
   // default Content-Type for unknown extensions, even though the
   // setFile()/setCacheableFile() response path falls back to
   // "text/plain". The divergence is intentional: it closes a rename-
   // bypass (renaming secret.zip to "secret" would otherwise resolve
   // to text/plain and silently skip the audit) at the cost of also
   // logging legitimate fetches of extensionless text files
   // (LICENSE, README, Makefile, etc.). Over-logging is the safer
   // default for an audit record.
   const std::string mimeType =
      filePath.getMimeContentType("application/octet-stream");
   if (mimeType.rfind("text/", 0) == 0  ||
       mimeType.rfind("image/", 0) == 0 ||
       mimeType.rfind("audio/", 0) == 0 ||
       mimeType.rfind("video/", 0) == 0 ||
       mimeType == "application/pdf"    ||
       mimeType == "application/json")
   {
      LOG_DEBUG_MESSAGE("Audit skipped (renderable Content-Type=" +
                        mimeType + "): " + filePath.getAbsolutePath());
      return false;
   }

   return true;
}

std::string createFileUrl(const core::FilePath& filePath)
{
   // determine url based on whether this is in ~ or not. Tag the result
   // with show=1 so handleFilesRequest / handleFileShow can distinguish
   // server-initiated preview/show flows from user-initiated downloads
   // and skip audit logging for the former.
   std::string url;
   if (isVisibleUserFile(filePath))
   {
      auto home = module_context::userHomePath();
      auto path = filePath.getRelativePath(home);
      url = "files/" + path + "?show=1";
   }
   else if (isPathViewAllowed(filePath))
   {
      url = "show" + filePath.getAbsolutePath() + "?show=1";
   }
   else
   {
      auto path = core::http::util::urlEncode(filePath.getAbsolutePath(), true);
      url = "file_show?path=" + path + "&show=1";
   }
   return url;
}


void showContent(const std::string& title, const core::FilePath& filePath)
{
   // first provision a content url
   std::string contentUrl = content_urls::provision(title, filePath);

   // fire event
   json::Object contentItem;
   contentItem["title"] = title;
   contentItem["contentUrl"] = contentUrl;
   ClientEvent event(client_events::kShowContent, contentItem);
   module_context::enqueClientEvent(event);
}


std::string resourceFileAsString(const std::string& fileName)
{
   FilePath resPath = session::options().rResourcesPath();
   FilePath filePath = resPath.completePath(fileName);
   std::string fileContents;
   Error error = readStringFromFile(filePath, &fileContents);
   if (error)
   {
      LOG_ERROR(error);
      return std::string();
   }

   return fileContents;
}

// given a pair of paths, return the second in the context of the first
std::string pathRelativeTo(const FilePath& sourcePath,
                           const FilePath& targetPath)
{
   std::string relative;
   if (targetPath == sourcePath)
   {
      relative = ".";
   }
   else if (targetPath.isWithin(sourcePath))
   {
      relative = targetPath.getRelativePath(sourcePath);
   }
   else
   {
      relative = createAliasedPath(targetPath);
   }
   return relative;
}

void activatePane(const std::string& pane)
{
   ClientEvent event(client_events::kActivatePane, pane);
   module_context::enqueClientEvent(event);
}

FilePath shellWorkingDirectory()
{
   std::string initialDirSetting = prefs::userPrefs().terminalInitialDirectory();
   if (initialDirSetting == kTerminalInitialDirectoryProject)
   {
      if (projects::projectContext().hasProject())
         return projects::projectContext().directory();
      else
         return module_context::safeCurrentPath();
   }
   else if (initialDirSetting == kTerminalInitialDirectoryCurrent)
      return module_context::safeCurrentPath();
   else if (initialDirSetting == kTerminalInitialDirectoryHome)
      return system::User::getUserHomePath();
   else
      return module_context::safeCurrentPath();
}

Events& events()
{
   static Events instance;
   return instance;
}

core::system::ProcessSupervisor& processSupervisor()
{
   static core::system::ProcessSupervisor instance;
   return instance;
}

FilePath sourceDiagnostics()
{
   FilePath diagnosticsPath =
         options().coreRSourcePath().completeChildPath("Diagnostics.R");
   
   Error error = r::exec::RFunction("source")
         .addParam(string_utils::utf8ToSystem(diagnosticsPath.getAbsolutePath()))
         .addParam("chdir", true)
         .call();
   
   if (error)
   {
      LOG_ERROR(error);
      return FilePath();
   }
   else
   {
      // note this path is also in Diagnostics.R so changes to the path
      // need to be synchronized there
      std::string reportPath = core::system::getenv("RSTUDIO_DIAGNOSTICS_REPORT");
      if (reportPath.empty())
         reportPath = "~/rstudio-diagnostics/diagnostics-report.txt";
      return module_context::resolveAliasedPath(reportPath);
   }
}
   
namespace {

void beginRpcHandler(json::JsonRpcFunction function,
                     json::JsonRpcRequest request,
                     std::string asyncHandle)
{
   try
   {
      json::JsonRpcResponse response;
      Error error = function(request, &response);
      BOOST_ASSERT(!response.hasAfterResponse());
      if (error)
         response.setError(error);
      
      if (!response.hasField(kEventsPending))
         response.setField(kEventsPending, "false");
      
      json::Object value;
      value["handle"] = asyncHandle;
      value["response"] = response.getRawResponse();
      ClientEvent evt(client_events::kAsyncCompletion, value);
      enqueClientEvent(evt);
   }
   CATCH_UNEXPECTED_EXCEPTION

}

} // anonymous namespace

core::Error executeAsync(const json::JsonRpcFunction& function,
                         const json::JsonRpcRequest& request,
                         json::JsonRpcResponse* pResponse)
{
   // Immediately return a response to the server with a handle that
   // identifies this invocation. In the meantime, kick off the actual
   // operation on a new thread.

   std::string handle = core::system::generateUuid(true);
   core::thread::safeLaunchThread(bind(beginRpcHandler,
                                       function,
                                       request,
                                       handle));
   pResponse->setAsyncHandle(handle);
   return Success();
}

core::json::Object compileOutputAsJson(const CompileOutput& compileOutput)
{
   json::Object compileOutputJson;
   compileOutputJson["type"] = compileOutput.type;
   compileOutputJson["output"] = compileOutput.output;
   return compileOutputJson;
}

std::string CRANReposURL()
{
   std::string url;
   r::exec::evaluateString("getOption('repos')[['CRAN']]", &url);
   if (url.empty())
      url = prefs::userPrefs().getCRANMirror().url;
   return url;
}

std::string rstudioCRANReposURL()
{
   std::string protocol = prefs::userPrefs().useSecureDownload() ?
                                                           "https" : "http";
   return protocol + "://cran.rstudio.com/";
}

SEXP rs_rstudioCRANReposUrl()
{
   r::sexp::Protect rProtect;
   return r::sexp::create(rstudioCRANReposURL(), &rProtect);
}

std::string downloadFileMethod(const std::string& defaultMethod)
{
   std::string method;
   Error error = r::exec::evaluateString(
                           "getOption('download.file.method', '" +
                            defaultMethod + "')", &method);
   if (error)
      LOG_ERROR(error);
   return method;
}

std::string CRANDownloadOptions()
{
   std::string options;
   Error error = r::exec::RFunction(".rs.CRANDownloadOptionsString").call(&options);
   if (error)
      LOG_ERROR(error);
   return options;
}

bool haveSecureDownloadFileMethod()
{
   bool secure = false;
   Error error = r::exec::RFunction(".rs.haveSecureDownloadFileMethod").call(
                                                                      &secure);
   if (error)
      LOG_ERROR(error);
   return secure;
}

shell_utils::ShellCommand RCommand::buildRCmd(const core::FilePath& rBinDir)
{
#if defined(_WIN32)
   shell_utils::ShellCommand rCmd(rBinDir.completeChildPath("Rcmd.exe"));
#else
   shell_utils::ShellCommand rCmd(rBinDir.completeChildPath("R"));
   rCmd << "CMD";
#endif
   return rCmd;
}

core::Error recursiveCopyDirectory(const core::FilePath& fromDir,
                                   const core::FilePath& toDir)
{
   using namespace string_utils;
   r::exec::RFunction fileCopy("file.copy");
   fileCopy.addParam("from", utf8ToSystem(fromDir.getAbsolutePath()));
   fileCopy.addParam("to", utf8ToSystem(toDir.getAbsolutePath()));
   fileCopy.addParam("recursive", true);
   return fileCopy.call();
}

bool isSessionTempPath(FilePath filePath)
{
   // get the real path
   Error error = core::system::realPath(filePath, &filePath);
   if (error)
      LOG_ERROR(error);

   // get the session temp dir real path; needed since the file path above is
   // also a real path--e.g. on OS X, it refers to /private/tmp rather than
   // /tmp
   FilePath tempDir;
   error = core::system::realPath(module_context::tempDir(), &tempDir);
   if (error)
      LOG_ERROR(error);

   return filePath.isWithin(tempDir);
}

namespace {

bool isUsingRHelpServer()
{
   // don't use R help server in server mode
   if (session::options().programMode() == kSessionProgramModeServer)
   {
      return false;
   }

#ifdef _WIN32
   // There is a known issue serving content from the session temporary folder
   // on R 4.0.0 for Windows:
   //
   //    https://github.com/rstudio/rstudio/issues/6737
   //
   // so we avoid using the help server in that case.
   if (r::util::hasExactVersion("4.0.0"))
   {
      return false;
   }
#endif

   // we're running on desktop with a suitable version of R;
   // okay to use the help server
   return true;
}

} // end anonymous namespace

std::string sessionTempDirUrl(const std::string& sessionTempPath)
{
   static bool useRHelpServer = isUsingRHelpServer();

   if (useRHelpServer)
   {
      // use the loopback IP address rather than 'localhost' here, since R's
      // help server binds explicitly to 127.0.0.1 -- on machines where
      // 'localhost' resolves elsewhere (e.g. only to '::1'), connections
      // to the help server would otherwise fail (#17579)
      return fmt::format("http://127.0.0.1:{}/session/{}", rLocalHelpPort(), sessionTempPath);
   }
   else
   {
      return fmt::format("session/{}", sessionTempPath);
   }
}

bool isPathViewAllowed(const FilePath& filePath)
{
   // Check to see if restrictions are in place
   if (!options().restrictDirectoryView())
      return true;

   // No paths are restricted in desktop mode
   if (options().programMode() != kSessionProgramModeServer)
      return true;

   // Viewing content in the home directory is always allowed
   if (filePath.isWithin(userHomePath().getParent()))
      return true;
      
   // Viewing content in the session temporary files path is always allowed
   if (isSessionTempPath(filePath))
      return true;

   // Allow users to view the system's configuration
   if (filePath.isWithin(core::system::xdg::systemConfigDir()))
      return true;

   // Viewing content in R libraries is always allowed
   std::vector<FilePath> libPaths = getLibPaths();
   for (auto&& libPath : libPaths)
   {
      if (filePath.isWithin(libPath))
      {
         return true;
      }
   }

   // Check session option for explicitly allowed directories
   std::string allowDirsPref = session::options().directoryViewAllowList();
   if (!allowDirsPref.empty())
   {
      auto allowDirs = core::algorithm::split(allowDirsPref, ":");
      for (auto&& allowDir : allowDirs)
      {
         if (filePath.isWithin(FilePath(allowDir)))
         {
            return true;
         }
      }
   }

   // All other paths are implicitly disallowed
   return false;
}

namespace {

bool hasStem(const FilePath& filePath, const std::string& stem)
{
   return filePath.getStem() == stem;
}

} // anonymous namespace

Error uniqueSaveStem(const core::FilePath& directoryPath,
                     const std::string& base,
                     std::string* pStem)
{
   return uniqueSaveStem(directoryPath, base, "", pStem);
}

Error uniqueSaveStem(const FilePath& directoryPath,
                     const std::string& base,
                     const std::string& delimiter,
                     std::string* pStem)
{
   // determine unique file name
   std::vector<FilePath> children;
   Error error = directoryPath.getChildren(children);
   if (error)
      return error;

   // search for unique stem
   int i = 0;
   *pStem = base;
   while(true)
   {
      // seek stem
      std::vector<FilePath>::const_iterator it = std::find_if(
                                                children.begin(),
                                                children.end(),
                                                boost::bind(hasStem, _1, *pStem));
      // break if not found
      if (it == children.end())
         break;

      // update stem and search again
      boost::format fmt(base + delimiter + "%1%");
      *pStem = boost::str(fmt % boost::io::group(std::setfill('0'),
                                                 std::setw(2),
                                                 ++i));
   }

   return Success();
}

json::Object plotExportFormat(const std::string& name,
                              const std::string& extension)
{
   json::Object formatJson;
   formatJson["name"] = name;
   formatJson["extension"] = extension;
   return formatJson;
}

Error createSelfContainedHtml(const FilePath& sourceFilePath,
                              const FilePath& targetFilePath)
{
   r::exec::RFunction func(".rs.pandocSelfContainedHtml");
   func.addParam(string_utils::utf8ToSystem(sourceFilePath.getAbsolutePath()));
   func.addParam(string_utils::utf8ToSystem(
      session::options().rResourcesPath().completePath("pandoc_template.html").getAbsolutePath()));
   func.addParam(string_utils::utf8ToSystem(targetFilePath.getAbsolutePath()));
   return func.call();
}

bool isUserFile(const FilePath& filePath)
{
   if (projects::projectContext().hasProject())
   {
      // if we are in a package project then screen our src- files
      if (projects::projectContext().config().buildType ==
                                              r_util::kBuildTypePackage)
      {
          FilePath pkgPath = projects::projectContext().buildTargetPath();
          std::string pkgRelative = filePath.getRelativePath(pkgPath);
          if (boost::algorithm::starts_with(pkgRelative, "src-"))
             return false;
      }

      // screen our various virtual environment directories + standard content ignores
      FilePath projPath = projects::projectContext().directory();
      std::vector<std::string> dirs({"packrat/", "renv/"});
      const std::vector<FilePath>& ignoreDirs = module_context::ignoreContentDirs();
      std::transform(ignoreDirs.begin(), ignoreDirs.end(), std::back_inserter(dirs),
                     [&projPath](const FilePath& ignorePath) {
         return ignorePath.getRelativePath(projPath) + "/";
      });
      std::string pkgRelative = filePath.getRelativePath(projPath);
      for (const auto& dir : dirs) {
         if (boost::algorithm::starts_with(pkgRelative, dir))
            return false;
      }
   }

   return true;
}

namespace {

// NOTE: sync changes with SessionCompilePdf.cpp logEntryJson
json::Value sourceMarkerJson(const SourceMarker& sourceMarker)
{
   json::Object obj;
   obj["type"] = static_cast<int>(sourceMarker.type);
   obj["path"] = module_context::createAliasedPath(sourceMarker.path);
   obj["line"] = sourceMarker.line;
   obj["column"] = sourceMarker.column;
   obj["message"] = sourceMarker.message.text();
   obj["message_is_html"] = sourceMarker.messageIsHtml;
   obj["log_path"] = "";
   obj["log_line"] = -1;
   obj["show_error_list"] = sourceMarker.showErrorList;
   return std::move(obj);
}

} // anonymous namespace

json::Array sourceMarkersAsJson(const std::vector<SourceMarker>& markers)
{
   json::Array markersJson;
   std::transform(markers.begin(),
                  markers.end(),
                  std::back_inserter(markersJson),
                  sourceMarkerJson);
   return markersJson;
}

SourceMarker::Type sourceMarkerTypeFromString(const std::string& type)
{
   if (type == "error")
      return SourceMarker::Error;
   else if (type == "warning")
      return SourceMarker::Warning;
   else if (type == "box")
      return SourceMarker::Box;
   else if (type == "info")
      return SourceMarker::Info;
   else if (type == "style")
      return SourceMarker::Style;
   else if (type == "usage")
      return SourceMarker::Usage;
   else
      return SourceMarker::Error;
}

core::json::Array sourceMarkersAsJson(const std::vector<SourceMarker>& markers);

bool isLoadBalanced()
{
   return !core::system::getenv(kRStudioSessionRoute).empty();
}

namespace {

#ifdef __APPLE__
void warnXcodeLicense()
{
   const char* msg =
R"EOF(Warning: macOS is reporting that you have not yet agreed to the Xcode license.
This can occur if Xcode has been updated or reinstalled (e.g. as part of a macOS update).
Some features (e.g. Git / SVN) may be disabled.

Please run:

    sudo xcodebuild -license accept

in a terminal to accept the Xcode license, and then restart RStudio.
)EOF";
   
   std::cerr << msg << std::endl;
}
#endif

} // end anonymous namespace

bool isMacOS()
{
#ifdef __APPLE__
   return true;
#else
   return false;
#endif
}

bool hasMacOSDeveloperTools()
{
   if (!isMacOS())
      return false;
   
   core::system::ProcessResult result;
   Error error = core::system::runCommand(
            "/usr/bin/xcrun --find --show-sdk-path",
            core::system::ProcessOptions(),
            &result);

   if (error)
   {
      LOG_ERROR(error);
      return false;
   }

   if (result.exitStatus == 69)
      checkXcodeLicense();

   return result.exitStatus == 0;
}

bool hasMacOSCommandLineTools()
{
   if (!isMacOS())
      return false;
   
   return FilePath("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk").exists();
}

void checkXcodeLicense()
{
#ifdef __APPLE__
   
   // avoid repeatedly warning the user
   static bool s_licenseChecked;
   if (s_licenseChecked)
      return;
   
   s_licenseChecked = true;
   
   core::system::ProcessResult result;
   Error error = core::system::runCommand(
            "/usr/bin/xcrun --find --show-sdk-path",
            core::system::ProcessOptions(),
            &result);
   
   // if an error occurs, log it but avoid otherwise annoying the user
   if (error)
   {
      LOG_ERROR(error);
      return;
   }
   
   // exit code 69 implies license error
   if (result.exitStatus == 69)
      warnXcodeLicense();
   
#endif
}

bool shouldIgnoreOutputDir(const FilePath& base, const std::string& outputDir)
{
   if (outputDir.empty())
      return false;

   // Compare by filesystem identity rather than lexically-normal string: a
   // configured 'output-dir: .' resolves to base, but 'base/.' does not
   // normalize to 'base' as a string. Ignoring base would drop all project
   // content, leaving Find in Files with no results (#17900).
   FilePath outputDirPath = base.completeChildPath(outputDir);
   return !outputDirPath.isEquivalentTo(base);
}

std::vector<FilePath> ignoreContentDirs()
{
   std::vector<FilePath> ignoreDirs;

   if (projects::projectContext().hasProject())
   {
      // python virtual environments
      ignoreDirs = projects::projectContext().pythonEnvs();
      quarto::QuartoConfig quartoConf = quarto::quartoConfig();

      auto addOutputDir = [&ignoreDirs](const FilePath& base, const std::string& outputDir)
      {
         if (shouldIgnoreOutputDir(base, outputDir))
            ignoreDirs.push_back(base.completeChildPath(outputDir));
      };

      // quarto site output dir
      if (quartoConf.is_project)
      {
         FilePath quartoProjDir = module_context::resolveAliasedPath(quartoConf.project_dir);
         addOutputDir(quartoProjDir, quartoConf.project_output_dir);
         ignoreDirs.push_back(quartoProjDir.completeChildPath("_freeze"));
      }

      // rmarkdown site output dir
      if (module_context::isWebsiteProject())
      {
         FilePath buildTargetPath = projects::projectContext().buildTargetPath();
         addOutputDir(buildTargetPath, module_context::websiteOutputDir());
      }
   }
   
   return ignoreDirs;
}

bool isIgnoredContent(const core::FilePath& filePath,
                      const std::vector<core::FilePath>& ignoreDirs)
{
   auto it = std::find_if(ignoreDirs.begin(), ignoreDirs.end(), [&filePath](const FilePath& dir) {
      return dir.exists() && filePath.isWithin(dir);
   });
   return it != ignoreDirs.end();
}


std::string getActiveLanguage()
{
   if (modules::reticulate::isReplActive())
   {
      return "Python";
   }
   else
   {
      return "R";
   }
}

Error adaptToLanguage(const std::string& language)
{
   // check to see what language is active in main console
   using namespace r::exec;
   
   // check to see what language is currently active (but default to r)
   std::string activeLanguage = getActiveLanguage();

   // now, detect if we are transitioning languages
   if (language != activeLanguage)
   {
      // since it may take some time for the console input to be processed,
      // we screen out consecutive transition attempts (otherwise we can
      // get multiple interleaved attempts to launch the REPL with console
      // input)
      static RSTUDIO_BOOST_CONNECTION conn;
      if (conn.connected())
         return Success();
      
      // establish the connection, and then simply disconnect once we
      // receive the signal
      conn = module_context::events().onConsolePrompt.connect([&](const std::string&) {
         conn.disconnect();
      });
      
      Error error;

      if (activeLanguage == "R" && language == "Python")
      {
         // r -> python: activate the reticulate REPL
         error = module_context::enqueueConsoleInput("reticulate::repl_python()");
      }
      else if (activeLanguage == "Python" && language == "R")
      {
         // python -> r: deactivate the reticulate REPL
         error = module_context::enqueueConsoleInput("quit");
      }

      if (error)
         LOG_ERROR(error);
   }
   
   return Success();
}

Error initialize()
{
   // register .Call methods
   RS_REGISTER_CALL_METHOD(rs_activatePane);
   RS_REGISTER_CALL_METHOD(rs_base64decode);
   RS_REGISTER_CALL_METHOD(rs_base64encode);
   RS_REGISTER_CALL_METHOD(rs_base64encodeFile);
   RS_REGISTER_CALL_METHOD(rs_htmlEscape);
   RS_REGISTER_CALL_METHOD(rs_enqueClientEvent);
   RS_REGISTER_CALL_METHOD(rs_ensureFileHidden);
   RS_REGISTER_CALL_METHOD(rs_generateShortUuid);
   RS_REGISTER_CALL_METHOD(rs_getPersistentValue);
   RS_REGISTER_CALL_METHOD(rs_isRScriptInPackageBuildTarget);
   RS_REGISTER_CALL_METHOD(rs_logErrorMessage);
   RS_REGISTER_CALL_METHOD(rs_logWarningMessage);
   RS_REGISTER_CALL_METHOD(rs_logInfoMessage);
   RS_REGISTER_CALL_METHOD(rs_markdownToHTML);
   RS_REGISTER_CALL_METHOD(rs_packageLoaded);
   RS_REGISTER_CALL_METHOD(rs_packageNameForSourceFile);
   RS_REGISTER_CALL_METHOD(rs_packageUnloaded);
   RS_REGISTER_CALL_METHOD(rs_resolveAliasedPath);
   RS_REGISTER_CALL_METHOD(rs_restartR);
   RS_REGISTER_CALL_METHOD(rs_rstudioCitation);
   RS_REGISTER_CALL_METHOD(rs_rstudioCRANReposUrl);
   RS_REGISTER_CALL_METHOD(rs_rstudioEdition);
   RS_REGISTER_CALL_METHOD(rs_rstudioProgramMode);
   RS_REGISTER_CALL_METHOD(rs_previewAllowedFunctions);
   RS_REGISTER_CALL_METHOD(rs_rstudioVersion);
   RS_REGISTER_CALL_METHOD(rs_rstudioLongVersion);
   RS_REGISTER_CALL_METHOD(rs_rstudioReleaseName);
   RS_REGISTER_CALL_METHOD(rs_sessionModulePath);
   RS_REGISTER_CALL_METHOD(rs_setPersistentValue);
   RS_REGISTER_CALL_METHOD(rs_showErrorMessage);
   RS_REGISTER_CALL_METHOD(rs_sourceDiagnostics);
   RS_REGISTER_CALL_METHOD(rs_threadSleep);
   RS_REGISTER_CALL_METHOD(rs_userPrompt);
   RS_REGISTER_CALL_METHOD(rs_setRpcDelay);
   RS_REGISTER_CALL_METHOD(rs_performBackgroundProcessing);

   // initialize monitored scratch dir
   initializeMonitoredUserScratchDir();

   // source the ModuleTools.R file
   FilePath modulesPath = session::options().modulesRSourcePath();
   return r::sourceManager().sourceTools(modulesPath.completePath("ModuleTools.R"));
}


} // namespace module_context         
} // namespace session
} // namespace rstudio
