Skip to content

Commit

Permalink
Merge pull request #569 from maleadt/execve
Browse files Browse the repository at this point in the history
launcher: replace parent process on supported platforms.
  • Loading branch information
davidanthoff authored Jan 11, 2024
2 parents 75f0dfc + 4eb8034 commit abaf9da
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 38 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ reqwest = { version = "0.11.18", default-features = false, features = ["blocking
reqwest = { version = "0.11.18", default-features = false, features = ["blocking", "rustls-tls-native-roots", "socks"] }

[target.'cfg(not(windows))'.dependencies]
nix = "0.27.1"
nix = { version = "0.27.1", features = ["process"] }

[build-dependencies]
anyhow = "1.0.72"
Expand Down
122 changes: 85 additions & 37 deletions src/bin/julialauncher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ use juliaup::config_file::{load_config_db, JuliaupConfig, JuliaupConfigChannel};
use juliaup::global_paths::get_paths;
use juliaup::jsonstructs_versionsdb::JuliaupVersionDB;
use juliaup::versions_file::load_versions_db;
#[cfg(not(windows))]
use nix::{
sys::wait::{waitpid, WaitStatus},
unistd::{fork, ForkResult},
};
use normpath::PathExt;
#[cfg(not(windows))]
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::path::PathBuf;

Expand Down Expand Up @@ -292,57 +299,98 @@ fn run_app() -> Result<i32> {
}
}

// We set a Ctrl-C handler here that just doesn't do anything, as we want the Julia child
// process to handle things.
ctrlc::set_handler(|| ()).with_context(|| "Failed to set the Ctrl-C handler.")?;
// On *nix platforms we replace the current process with the Julia one.
// This simplifies use in e.g. debuggers, but requires that we fork off
// a subprocess to do the selfupdate and versiondb update.
#[cfg(not(windows))]
match unsafe { fork() } {
// NOTE: It is unsafe to perform async-signal-unsafe operations from
// forked multithreaded programs, so for complex functionality like
// selfupdate to work julialauncher needs to remain single-threaded.
// Ref: https://docs.rs/nix/latest/nix/unistd/fn.fork.html#safety
Ok(ForkResult::Parent { child, .. }) => {
// wait for the daemon-spawning child to finish
match waitpid(child, None) {
Ok(WaitStatus::Exited(_, code)) => {
if code != 0 {
panic!("Could not fork (child process exited with code: {})", code)
}
}
Ok(_) => {
panic!("Could not fork (child process did not exit normally)");
}
Err(e) => {
panic!("Could not fork (error waiting for child process, {})", e);
}
}

let mut child_process = std::process::Command::new(julia_path)
.args(&new_args)
.spawn()
.with_context(|| "The Julia launcher failed to start Julia.")?; // TODO Maybe include the command we actually tried to start?
// replace the current process
std::process::Command::new(julia_path)
.args(&new_args)
.exec();

run_versiondb_update(&config_file).with_context(|| "Failed to run version db update")?;
// this is never reached
Ok(0)
}
Ok(ForkResult::Child) => {
// double-fork to prevent zombies
match unsafe { fork() } {
Ok(ForkResult::Parent { child: _, .. }) => {
// we don't do anything here so that this process can be
// reaped immediately
}
Ok(ForkResult::Child) => {
// this is where we perform the actual work. we don't do
// any typical daemon-y things (like detaching the TTY)
// so that any error output is still visible.

run_selfupdate(&config_file).with_context(|| "Failed to run selfupdate.")?;
// We set a Ctrl-C handler here that just doesn't do anything, as we want the Julia child
// process to handle things.
ctrlc::set_handler(|| ())
.with_context(|| "Failed to set the Ctrl-C handler.")?;

let status = child_process
.wait()
.with_context(|| "Failed to wait for Julia process to finish.")?;
run_versiondb_update(&config_file)
.with_context(|| "Failed to run version db update")?;

let code = match status.code() {
Some(code) => code,
None => {
#[cfg(not(windows))]
{
use std::os::unix::process::ExitStatusExt;
run_selfupdate(&config_file).with_context(|| "Failed to run selfupdate.")?;
}
Err(_) => panic!("Could not double-fork"),
}

let signal = status.signal();
Ok(0)
}
Err(_) => panic!("Could not fork"),
}

if let Some(signal) = signal {
let signal = nix::sys::signal::Signal::try_from(signal)
.with_context(|| format!("Unknown signal value {}.", signal))?;
// On other platforms (i.e., Windows) we just spawn a subprocess
#[cfg(windows)]
{
// We set a Ctrl-C handler here that just doesn't do anything, as we want the Julia child
// process to handle things.
ctrlc::set_handler(|| ()).with_context(|| "Failed to set the Ctrl-C handler.")?;

nix::sys::signal::raise(signal).with_context(|| "Failed to raise signal.")?;
let mut child_process = std::process::Command::new(julia_path)
.args(&new_args)
.spawn()
.with_context(|| "The Julia launcher failed to start Julia.")?; // TODO Maybe include the command we actually tried to start?

// We raise the signal twice because SIGSEGV and SIGBUS require that
// https://github.com/JuliaLang/juliaup/pull/525#issuecomment-1353526900
// https://github.com/rust-lang/rust/blob/984eab57f708e62c09b3d708033fe620130b5f39/library/std/src/sys/unix/stack_overflow.rs#L60-L80
nix::sys::signal::raise(signal).with_context(|| "Failed to raise signal.")?;
run_versiondb_update(&config_file).with_context(|| "Failed to run version db update")?;

anyhow::bail!("Maybe we should never reach this?");
} else {
anyhow::bail!("We weren't able to extract a signal, this should never happen.");
}
}
run_selfupdate(&config_file).with_context(|| "Failed to run selfupdate.")?;

#[cfg(windows)]
{
let status = child_process
.wait()
.with_context(|| "Failed to wait for Julia process to finish.")?;

let code = match status.code() {
Some(code) => code,
None => {
anyhow::bail!("There is no exit code, that should not be possible on Windows.");
}
}
};
};

Ok(code)
Ok(code)
}
}

fn main() -> Result<std::process::ExitCode> {
Expand Down

0 comments on commit abaf9da

Please sign in to comment.