use std::ffi::OsString;
use std::process::ExitStatus;
use std::sync::Arc;
use std::time::Duration;
use crate::config::lock::ProfileLock;
use crate::config::name::ProfileName;
use crate::config::store::Store;
use crate::error::Error;
use crate::git::remote_url::Scheme;
use crate::git::repo::Repo;
use crate::git::runner::{Git, exit_code, spawn_error};
use crate::keys::store::{DeviceSecrets, KeyStore};
use crate::proxy::Proxy;
use crate::proxy::git_env::{self, SshSettings};
use crate::transport::{self, Transport};
use super::network::AwayPlan;
pub(crate) const SHUTDOWN_GRACE: Duration = Duration::from_secs(1);
pub fn run(plan: AwayPlan) -> Result<i32, Error> {
if plan.remote.scheme == Scheme::Git {
return Err(Error::msg(format!(
"git:// remotes can't go through the tunnel. Use the server's ssh:// or https:// \
URL instead (`git remote set-url <remote> <url>`). Profile `{}`",
plan.name
)));
}
let store = super::open_store()?;
let name = &plan.name;
let (_lock, secrets) = take_tunnel(&store, name)?;
let repo = Repo::new(Git::new(&plan.global_options));
let core_ssh_command = repo.ssh_command()?;
let exe = std::env::current_exe()
.map_err(|e| Error::msg(format!("couldn't find gitaway's own path: {e}")))?;
let runtime = runtime()?;
let result = runtime.block_on(async {
let transport: Arc<dyn Transport> =
Arc::from(transport::open(name.as_str(), &plan.profile, secrets).await?);
let proxy = Proxy::start(transport, plan.profile.hosts.clone())
.await
.map_err(|e| Error::msg(format!("couldn't start the local proxy: {e}")))?;
let git_ssh_command = std::env::var("GIT_SSH_COMMAND").ok();
let git_ssh = std::env::var("GIT_SSH").ok();
let ssh = SshSettings {
git_ssh_command: git_ssh_command.as_deref(),
core_ssh_command: core_ssh_command.as_deref(),
git_ssh: git_ssh.as_deref(),
};
let existing_count = std::env::var("GIT_CONFIG_COUNT").ok();
let env = git_env::build(
proxy.address(),
proxy.token(),
&exe,
ssh,
existing_count.as_deref(),
)
.map_err(Error::msg)?;
let status = run_git(&plan.args, &env).await?;
let code = exit_code(status);
if code != 0 {
for failure in proxy.failures() {
eprintln!("gitaway: {failure}");
}
}
Ok(code)
});
runtime.shutdown_timeout(SHUTDOWN_GRACE);
result
}
pub(crate) fn take_tunnel(
store: &Store,
name: &ProfileName,
) -> Result<(ProfileLock, DeviceSecrets), Error> {
let lock = ProfileLock::acquire(store.dir(), name, || {
eprintln!(
"gitaway: another gitaway command is using profile `{name}`'s tunnel; waiting for it to finish…"
);
})
.map_err(|e| Error::msg(e.to_string()))?;
let keys = KeyStore::from_env(store.dir().clone())?;
let secrets = keys.load(name)?.ok_or_else(|| {
Error::msg(format!(
"profile `{name}` has no device key, so it can't open the tunnel. \
Create one with `gitaway key rotate {name}` and give the server its public key"
))
})?;
Ok((lock, secrets))
}
pub(crate) fn runtime() -> Result<tokio::runtime::Runtime, Error> {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.map_err(|e| Error::msg(format!("couldn't start the tunnel's runtime: {e}")))
}
async fn run_git(args: &[OsString], env: &git_env::GitEnv) -> Result<ExitStatus, Error> {
let mut command = Git::new(&[]).command(args);
for (name, value) in &env.set {
command.env(name, value);
}
for name in &env.remove {
command.env_remove(name);
}
let mut child = tokio::process::Command::from(command)
.spawn()
.map_err(spawn_error)?;
wait(&mut child)
.await
.map_err(|e| Error::msg(format!("lost track of git: {e}")))
}
#[cfg(unix)]
async fn wait(child: &mut tokio::process::Child) -> std::io::Result<ExitStatus> {
use tokio::signal::unix::{SignalKind, signal};
let mut interrupt = signal(SignalKind::interrupt())?;
let mut quit = signal(SignalKind::quit())?;
let mut hangup = signal(SignalKind::hangup())?;
let mut terminate = signal(SignalKind::terminate())?;
loop {
tokio::select! {
status = child.wait() => return status,
_ = interrupt.recv() => {}
_ = quit.recv() => {}
_ = hangup.recv() => {}
_ = terminate.recv() => forward_terminate(child),
}
}
}
#[cfg(unix)]
fn forward_terminate(child: &tokio::process::Child) {
use rustix::process::{Pid, Signal, kill_process};
let pid = child
.id()
.and_then(|id| i32::try_from(id).ok())
.and_then(Pid::from_raw);
if let Some(pid) = pid {
let _ = kill_process(pid, Signal::TERM);
}
}
#[cfg(not(unix))]
async fn wait(child: &mut tokio::process::Child) -> std::io::Result<ExitStatus> {
loop {
tokio::select! {
status = child.wait() => return status,
_ = tokio::signal::ctrl_c() => {}
}
}
}