Overriding RStudio's Startup Hook inside Rprofile

From the R and RStudio in a Secured Environment on CentOS 7 and Improving the secured computer-based testing environment for R on CentOS 7 using Rprofile and Renviron posts, there was a need to override commands that required internet access in an internetless environment. One huge part of the puzzle that wasn’t quite solved was the ability to disable the install.packages() command within RStudio. In particular, the outcome from running install.packages() looked like:

Error message under a faked local CRAN.

In reality, the desired outcome was to provide a custom message:

Show the message "done" instead of an error on install attempt.

This resulted in an issue (4498) being filed that asked for a specific point during RStudio’s startup of R. Turns out that Jonathan McPherson already added it ~4+ years ago in commit fc86699#diff-9825eb7f38c890c5ed3121779b9f993e. He provided a sample use case of it in an earlier ticket requesting the feature. Therefore, the hook will work with RStudio 0.99.* and later versions.

With this being said, the post is broken down into four sections that aim to cover RStudio’s hook process, overriding a base function, establishing a hook, and setting up hooks to run automatically in startup with .Rprofile.

RStudio Hook

The injection point uses R’s setHook() function to register a value for the session initialization in RStudio. The example below can be found on RStudio’s rstudioapi R package website.

setHook("rstudio.sessionInit", function(newSession) {
  if (newSession)
    message("Welcome to RStudio! ", rstudioapi::getVersion())
}, action = "append")

Overriding a base command

Next, we need to be able to override or replace an existing function in an R package. Functions are able to be overriden by modifying the R package environment they reside in. Overwriting a function within a package requires unlocking the environment (“bind”), modifying the functions, and then relocking the environment. The environment write status can be modified with base::unlockBinding() and base::lockBinding() while the assignment is handled by base::assign().

Note: The approach is only for private use. CRAN will not permit any package modifying content in this way.

## Override an R function in a package.
shim_pkg_func = function(name, pkgname, value) {
  
  ## Ensure package is loaded.
  ## If the package is not on the search path, we cannot modify it!
  pkg_env_name = paste0("package:", pkgname)
  if (!pkg_env_name %in% search()) {
    # Load the library
    library(pkgname, character.only = TRUE)
  }
  
  # Retrieve package environment
  env = as.environment(pkg_env_name)

  # Unlock environment where the function/variable is found.
  base::unlockBinding(name, env)
  
  # Make the assignment into the environment with the new value
  base::assign(name, value, envir = env)
  
  # Close the environment
  base::lockBinding(name, env)
}

Establishing a Hook

The next part is to establish the hook regardless of whether R was being used in RStudio, RGUI, or in shell. Differentiating between applications is key. One way to do so is to check whether the RSTUDIO environment variable is set. Environment variable can be searched for using Sys.getenv() function.

## Provide an alternative install.packages(...) routine
install_packages_shim = function(...) { message("Done!") }

## Setup a shim to override the install.packages() function
if(Sys.getenv("RSTUDIO") == "1") {
  ## Establishes a delayed registration of the shim after
  ## RStudio's startup procedure occurs. This procedure overwrites settings
  ## sometimes established in .Rprofile. Thus, we need a delayed
  ## component to ensure we're preventing
  ## install.packages() from being delayed
  ## c.f. https://github.com/rstudio/rstudio/issues/1579#issuecomment-495706255
  setHook("rstudio.sessionInit", function(newSession) {
    if (newSession)
      shim_pkg_func("install.packages", "utils", install_packages_shim)
  }, action = "append")
} else {
  ## Establish the shim for _R_ terminal sessions or R GUI.
  ## Attempting to establish the hook with RStudio would result
  ## in its initialization procedure overwrite this shim.
  shim_pkg_func("install.packages", "utils", install_packages_shim)
}

Full Code

Combining each portion of the above code inside the .Rprofile gives the ideal startup override. That said, the code should be placed inside the .First() function, which runs when R is first loaded. For the full context, consider checking out the .Rprofile on GitHub.

To create an .Rprofile file, use:

touch ~/.Rprofile
vi ~/.Rprofile

Full code discussed previously with the .First() modification to be placed inside ~/.Rprofile.

.First = function() {

    ## Disable install.packages ----

    ## Override an R function in a package.
    shim_pkg_func = function(name, pkgname, value) {

        ## Ensure package is loaded.
        ## If the package is not on the search path, we cannot modify it!
        pkg_env_name = paste0("package:", pkgname)
        if (!pkg_env_name %in% search()) {
            # Load the library
            library(pkgname, character.only = TRUE)
        }

        # Retrieve package environment
        env = as.environment(pkg_env_name)
        #pkg_ns_env = asNamespace(pkgname)

        # Unlock environment where the function/variable is found.
        base::unlockBinding(name, env)

        # Make the assignment into the environment with the new value
        base::assign(name, value, envir = env)

        # Close the environment
        base::lockBinding(name, env)
    }

    ## Provide an alternative install.packages(...) routine
    install_packages_shim = function(...) { message("Done!") }

    ## Setup a shim to override the install.packages() function
    if(Sys.getenv("RSTUDIO") == "1") {
        setHook("rstudio.sessionInit", function(newSession) {
            if (newSession)
                shim_pkg_func("install.packages", "utils", install_packages_shim)
        }, action = "append")
    } else {
        ## Establish the shim for _R_ terminal sessions or R GUI.
        shim_pkg_func("install.packages", "utils", install_packages_shim)
    }
}

Fin

In conclusion, the hooks RStudio setups can be overriden with just a bit more code. Hopefully, this helps out either with auto-launching content, setting up new pranks, or just trying to avoid internet-facing functions from triggering.

comments powered by Disqus