#[cfg(test)]
mod tests;
use crate::external::rig::{self, RigError};
use std::ffi::{OsStr, OsString};
use std::process::Command;
#[cfg(not(unix))]
use std::process::Stdio;
use crate::repl::{arf_eprintln, arf_println};
const R_VERSION_ENV_VARS: &[&str] = &[
"R_HOME",
"LD_LIBRARY_PATH",
"RVENV",
"RVENV_UNSYNCED",
#[cfg(target_os = "macos")]
"DYLD_LIBRARY_PATH",
"R_LIBS_USER",
"R_LIBS_SITE",
"R_LIBS",
"R_SYSTEM_ABI",
"R_DOC_DIR",
"R_SHARE_DIR",
"R_INCLUDE_DIR",
];
const ALWAYS_REMOVE_ENV_VARS: &[&str] = &["LD_LIBRARY_PATH", "R_HOME", "RVENV", "RVENV_UNSYNCED"];
#[derive(Default)]
struct EnvChanges {
restored: Vec<(&'static str, OsString)>,
removed: Vec<&'static str>,
}
fn env_changes_for_switch<F>(version: Option<&str>, mut startup_value: F) -> EnvChanges
where
F: FnMut(&str) -> Option<OsString>,
{
let mut changes = EnvChanges::default();
if version.is_none() {
return changes;
}
for &var in R_VERSION_ENV_VARS {
if ALWAYS_REMOVE_ENV_VARS.contains(&var) {
changes.removed.push(var);
} else if let Some(value) = startup_value(var) {
changes.restored.push((var, value));
} else {
changes.removed.push(var);
}
}
changes
}
fn arf_supplied_wrapper_paths_to_remove<FStartup, FCurrent>(
wrapper_values: impl IntoIterator<Item = (&'static str, OsString)>,
mut startup_value: FStartup,
mut current_value: FCurrent,
) -> Vec<&'static str>
where
FStartup: FnMut(&str) -> Option<OsString>,
FCurrent: FnMut(&str) -> Option<OsString>,
{
wrapper_values
.into_iter()
.filter_map(|(name, expected)| {
(startup_value(name).is_none() && current_value(name).as_ref() == Some(&expected))
.then_some(name)
})
.collect()
}
fn build_restart_command(
exe: &std::path::Path,
args: &[OsString],
changes: &EnvChanges,
startup_env_carrier: &str,
) -> Command {
let mut cmd = Command::new(exe);
cmd.args(args);
cmd.env(crate::STARTUP_ENV_CARRIER, startup_env_carrier);
#[cfg(unix)]
if let Some(fd) = crate::pid_file::restart_fd_carrier() {
cmd.env(crate::pid_file::RESTART_PID_FD_ENV, fd);
}
for (var, value) in &changes.restored {
cmd.env(var, value);
}
for var in &changes.removed {
cmd.env_remove(var);
}
cmd
}
fn print_env_changes(changes: &EnvChanges) {
let removed: Vec<&str> = changes
.removed
.iter()
.copied()
.filter(|var| std::env::var_os(var).is_some())
.collect();
if changes.restored.is_empty() && removed.is_empty() {
return;
}
let mut details = Vec::new();
if !changes.restored.is_empty() {
details.push(format!(
"restored: {}",
changes
.restored
.iter()
.map(|(var, _)| *var)
.collect::<Vec<_>>()
.join(", ")
));
}
if !removed.is_empty() {
details.push(format!("removed: {}", removed.join(", ")));
}
arf_println!(
"Environment variables for the R restart: {}",
details.join("; ")
);
}
pub fn restart_process(version: Option<&str>) {
if let Some(ver) = version {
if let Err(message) = validate_rig_for_switch_with(rig::rig_available) {
for line in message.lines() {
arf_eprintln!("{}", line);
}
return;
}
match rig::resolve_version(ver) {
Ok(resolved) => {
log::info!(
"Switching to R version {} ({})",
resolved.version,
resolved.r_home
);
}
Err(e) => {
arf_eprintln!("Error: {}", e);
return;
}
}
}
let exe = match std::env::current_exe() {
Ok(e) => e,
Err(e) => {
arf_eprintln!("Error: Failed to get current executable: {}", e);
return;
}
};
let mut args: Vec<OsString> = crate::normalized_args();
if let Some(v) = &version {
args = args_with_r_version(args, v);
}
let mut changes = env_changes_for_switch(version, crate::startup_env_value);
if version.is_none()
&& let Some(old_r_home) = std::env::var_os("R_HOME")
{
changes.removed.extend(arf_supplied_wrapper_paths_to_remove(
arf_libr::r_path_vars_from_wrapper(std::path::Path::new(&old_r_home)),
crate::startup_env_value,
|name| std::env::var_os(name),
));
}
print_env_changes(&changes);
let startup_env_carrier = crate::startup_env_carrier();
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let _pid_fd_guard = match crate::pid_file::prepare_pid_fd_for_exec() {
Some(Ok(guard)) => Some(guard),
Some(Err(error)) => {
arf_eprintln!("Error: Failed to prepare PID file for restart: {error}");
return;
}
None => None,
};
let mut cmd = build_restart_command(&exe, &args, &changes, &startup_env_carrier);
let err = cmd.exec();
arf_eprintln!("Error: Failed to restart: {}", err);
}
#[cfg(not(unix))]
{
crate::ipc::stop_server();
if let Some(pid_path) = crate::pid_file::initial_pid_file_path()
&& let Err(e) = crate::pid_file::relinquish_pid_file_for_restart(&pid_path)
{
arf_eprintln!("Error: {}", e);
std::process::exit(1);
}
match build_restart_command(&exe, &args, &changes, &startup_env_carrier)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
{
Ok(mut child) => match child.wait() {
Ok(status) => std::process::exit(status.code().unwrap_or(1)),
Err(e) => {
arf_eprintln!("Error: Failed to wait for restarted process: {}", e);
std::process::exit(1);
}
},
Err(e) => {
arf_eprintln!("Error: Failed to restart: {}", e);
std::process::exit(1);
}
}
}
}
fn validate_rig_for_switch_with<FAvailable>(rig_available: FAvailable) -> Result<(), String>
where
FAvailable: FnOnce() -> Result<(), RigError>,
{
rig_available().map_err(|error| match error {
RigError::NotInstalled => {
"Error: rig is not installed. Cannot switch R versions.\nInstall rig from https://github.com/r-lib/rig".to_string()
}
RigError::CommandFailed(reason) => format!(
"Error: rig is installed but failed while checking availability: {reason}\nCannot switch R versions until rig is working."
),
error => format!(
"Error: Could not check whether rig is available: {error}\nCannot switch R versions until rig is working."
),
})
}
fn filter_r_version_args(args: Vec<OsString>) -> Vec<OsString> {
let mut result = Vec::new();
let mut index = 0;
while index < args.len() {
let arg = &args[index];
if arg == "--" {
result.extend(args[index..].iter().cloned());
break;
}
if arg == "--with-r-version" || arg == "--r-provider" {
index = (index + 2).min(args.len());
continue;
}
if arg_has_ascii_prefix(arg, b"--with-r-version=")
|| arg_has_ascii_prefix(arg, b"--r-provider=")
{
index += 1;
continue;
}
result.push(arg.clone());
index += 1;
}
result
}
fn args_with_r_version(args: Vec<OsString>, version: &str) -> Vec<OsString> {
let mut args = filter_r_version_args(args);
let separator = args
.iter()
.position(|arg| arg == "--")
.unwrap_or(args.len());
args.splice(
separator..separator,
[OsString::from("--with-r-version"), OsString::from(version)],
);
args
}
fn arg_has_ascii_prefix(arg: &OsStr, prefix: &[u8]) -> bool {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
arg.as_bytes().starts_with(prefix)
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
arg.encode_wide()
.take(prefix.len())
.eq(prefix.iter().copied().map(u16::from))
}
#[cfg(not(any(unix, windows)))]
{
arg.to_str()
.is_some_and(|arg| arg.as_bytes().starts_with(prefix))
}
}