#[cfg(unix)]
use std::process::{Command, Stdio};
#[cfg(unix)]
use std::time::{Duration, Instant};
use clap::Subcommand;
#[cfg(unix)]
use eyre::bail;
use eyre::{Result, WrapErr};
use url::Url;
use atuin_client::settings::Settings;
#[derive(Subcommand, Debug)]
pub enum Cmd {
Share {
#[arg(long)]
write: bool,
#[arg(long)]
yes: bool,
#[arg(long, hide = true)]
active: bool,
#[arg(long, hide = true, requires = "active")]
foreground: bool,
#[arg(
long,
conflicts_with_all = ["write", "yes", "active", "foreground", "url", "internal_daemon"]
)]
stop: bool,
#[arg(
long,
conflicts_with_all = ["write", "yes", "active", "foreground", "stop", "internal_daemon"]
)]
url: bool,
#[arg(long, hide = true, requires = "active", conflicts_with = "foreground")]
internal_daemon: bool,
},
}
impl Cmd {
#[cfg(unix)]
pub fn should_daemonize(&self) -> bool {
match self {
Self::Share {
internal_daemon, ..
} => *internal_daemon,
}
}
pub async fn run(self, settings: &Settings) -> Result<()> {
report_refusal(self.dispatch(settings).await)
}
async fn dispatch(self, settings: &Settings) -> Result<()> {
match self {
Self::Share {
write,
yes,
active,
foreground,
stop,
url,
internal_daemon,
} => {
#[cfg(unix)]
{
if stop {
return stop_share().await;
}
if url {
return print_share_url();
}
if active && !foreground && !internal_daemon {
return spawn_background_share(write, yes).await;
}
}
#[cfg(not(unix))]
let _ = (stop, url);
let hub_url = lab_ws_url(settings)?;
let api_token = lab_api_token(settings).await?;
Ok(atuin_lab_share::run_share(atuin_lab_share::ShareOptions {
yes,
write,
active,
foreground,
internal_daemon,
hub_url,
api_token,
})
.await?)
}
}
}
}
fn report_refusal(result: Result<()>) -> Result<()> {
if let Some(refusal @ atuin_lab_share::Error::ActiveShareUnsupported) = result
.as_ref()
.err()
.and_then(eyre::Report::downcast_ref::<atuin_lab_share::Error>)
{
eprintln!("error: {refusal}");
std::process::exit(1);
}
result
}
#[cfg(unix)]
const SPAWN_URL_BUDGET: Duration = Duration::from_secs(25);
#[cfg(unix)]
const SPAWN_LOCK_GRACE: Duration = Duration::from_secs(3);
#[cfg(unix)]
const SPAWN_POLL: Duration = Duration::from_millis(100);
#[cfg(unix)]
const STOP_TIMEOUT: Duration = Duration::from_secs(10);
#[cfg(unix)]
const NO_ACTIVE_SHARE: &str = "no active share session.";
#[cfg(unix)]
const SHARING_STOPPED: &str = "sharing stopped.";
#[cfg(unix)]
const STOP_TIMED_OUT: &str =
"sharing did not stop within 10s; the share process may still be running";
#[cfg(unix)]
const URL_STILL_CONNECTING: &str = "the share is still connecting; try again shortly";
#[cfg(unix)]
const SPAWN_TIMED_OUT: &str = "timed out waiting for the background share to connect; run \
`atuin lab share --active --foreground` to debug, or `atuin lab share --stop` to clean up";
#[cfg(unix)]
const SPAWN_CHILD_DIED: &str = "the background share exited before publishing a URL; run \
`atuin lab share --active --foreground` to see why";
#[cfg(unix)]
fn attach_copy(url: &str) -> String {
format!(
"Sharing this session at: {url}\n\
Run `atuin lab share --stop` to end sharing. `atuin lab share --url` reprints the link.\n\
This session has no warning bar; viewers stay connected until you stop or the shell exits."
)
}
#[cfg(unix)]
async fn spawn_background_share(write: bool, yes: bool) -> Result<()> {
use atuin_lab_share::lifecycle;
if !atuin_lab_share::preflight_active_share(write, yes)? {
return Ok(());
}
let pidfile = lifecycle::pidfile_path();
let url_file = lifecycle::url_file_path();
if lifecycle::probe_lock(&pidfile)? == lifecycle::LockState::Held {
return Err(atuin_lab_share::Error::ShareAlreadyRunning.into());
}
let spawn_id = atuin_common::utils::uuid_v7().as_simple().to_string();
let exe = std::env::current_exe().wrap_err("could not locate atuin executable")?;
let mut cmd = Command::new(exe);
cmd.args(["lab", "share", "--active", "--internal-daemon", "--yes"]);
if write {
cmd.arg("--write");
}
cmd.env(lifecycle::SPAWN_ID_ENV, &spawn_id);
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
let mut child = cmd
.spawn()
.wrap_err("failed to spawn the background share process")?;
let start = Instant::now();
loop {
let _ = child.try_wait();
if lifecycle::read_url_file_owner(&url_file).as_deref() == Some(spawn_id.as_str())
&& let Some(url) = lifecycle::read_url_file(&url_file)
{
println!("{}", attach_copy(&url));
return Ok(());
}
if start.elapsed() >= SPAWN_URL_BUDGET {
bail!(SPAWN_TIMED_OUT);
}
if start.elapsed() >= SPAWN_LOCK_GRACE
&& lifecycle::probe_lock(&pidfile)? == lifecycle::LockState::Free
{
bail!(SPAWN_CHILD_DIED);
}
tokio::time::sleep(SPAWN_POLL).await;
}
}
#[cfg(unix)]
async fn stop_share() -> Result<()> {
use atuin_lab_share::lifecycle;
let pidfile = lifecycle::pidfile_path();
let url_file = lifecycle::url_file_path();
if lifecycle::probe_lock(&pidfile)? == lifecycle::LockState::Free {
lifecycle::remove_url_file(&url_file);
let _ = std::fs::remove_file(&pidfile);
eprintln!("{NO_ACTIVE_SHARE}");
std::process::exit(1);
}
let Some(pid) = lifecycle::read_pidfile_pid(&pidfile) else {
bail!(
"a share is running but its pidfile at {} is unreadable",
pidfile.display()
);
};
let stopped_owner = lifecycle::read_url_file_owner(&url_file);
let stopped_url = lifecycle::read_url_file(&url_file);
let _ = Command::new("kill")
.args(["-TERM", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
if !lifecycle::wait_for_lock_release(&pidfile, STOP_TIMEOUT).await {
bail!(STOP_TIMED_OUT);
}
if lifecycle::read_url_file_owner(&url_file) == stopped_owner
&& lifecycle::read_url_file(&url_file) == stopped_url
{
lifecycle::remove_url_file(&url_file);
}
println!("{SHARING_STOPPED}");
Ok(())
}
#[cfg(unix)]
fn print_share_url() -> Result<()> {
use atuin_lab_share::lifecycle;
if lifecycle::probe_lock(&lifecycle::pidfile_path())? == lifecycle::LockState::Held {
match lifecycle::read_url_file(&lifecycle::url_file_path()) {
Some(url) => {
println!("{url}");
Ok(())
}
None => bail!(URL_STILL_CONNECTING),
}
} else {
eprintln!("{NO_ACTIVE_SHARE}");
std::process::exit(1);
}
}
fn lab_ws_url(settings: &Settings) -> Result<Url> {
if let Ok(u) = std::env::var("ATUIN_LAB_HUB_URL") {
return Url::parse(&u).wrap_err("ATUIN_LAB_HUB_URL is not a valid URL");
}
let mut url = settings.hub_endpoint();
let ws_scheme = if url.scheme() == "http" { "ws" } else { "wss" };
let _ = url.set_scheme(ws_scheme);
Ok(url)
}
async fn lab_api_token(settings: &Settings) -> Result<String> {
if let Ok(t) = std::env::var("ATUIN_LAB_HUB_TOKEN") {
return Ok(t);
}
settings
.hub_session_token()
.await
.wrap_err("not logged in to Atuin Hub -- run `atuin login` first")
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn attach_copy_is_pinned() {
let copy = attach_copy("https://hub.example/s/abc#key");
assert_eq!(
copy,
"Sharing this session at: https://hub.example/s/abc#key\n\
Run `atuin lab share --stop` to end sharing. `atuin lab share --url` reprints the link.\n\
This session has no warning bar; viewers stay connected until you stop or the shell exits."
);
assert!(copy.is_ascii());
}
#[test]
fn lifecycle_copy_is_pinned() {
assert_eq!(NO_ACTIVE_SHARE, "no active share session.");
assert_eq!(SHARING_STOPPED, "sharing stopped.");
for msg in [
NO_ACTIVE_SHARE,
SHARING_STOPPED,
STOP_TIMED_OUT,
URL_STILL_CONNECTING,
SPAWN_TIMED_OUT,
SPAWN_CHILD_DIED,
] {
assert!(msg.is_ascii(), "{msg:?} must stay pure ASCII");
}
}
}