use std::time::Duration;
use serde::Deserialize;
use tracing::{debug, info, warn};
use crate::{Namespace, Remote, Workspace};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum Status {
Watching,
Disconnected,
HaltedOnRootEmptied,
HaltedOnRootDeletion,
HaltedOnRootTypeChange,
#[serde(other)]
Unknown,
}
impl Status {
fn is_halted(&self) -> bool {
matches!(
self,
Status::HaltedOnRootEmptied
| Status::HaltedOnRootDeletion
| Status::HaltedOnRootTypeChange
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct SessionEntry {
status: Status,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Session {
name: String,
}
impl Session {
pub fn start(
remote: &Remote,
workspace: &Workspace,
namespace: &Namespace,
) -> eyre::Result<Self> {
let workspace = workspace.display().to_string();
let remote = format!("{remote}:/tmp/mirage/{ns}", ns = namespace.display());
let name = namespace
.display()
.to_string()
.replace('/', "-")
.replace('@', "-at-");
info!(session = %name, target = %remote, "creating mutagen sync session");
let status = cmd::create(&workspace, &remote, &name)?;
if !status.success() {
eyre::bail!("mutagen sync create exited with {status}");
}
let session = Self { name };
session.wait_until_watching()?;
Ok(session)
}
pub fn terminate(self) -> eyre::Result<()> {
info!(session = %self.name, "terminating mutagen sync session");
let status = cmd::terminate(&self.name)?;
if !status.success() {
eyre::bail!("mutagen sync terminate exited with {status}");
}
std::mem::forget(self);
Ok(())
}
fn wait_until_watching(&self) -> eyre::Result<()> {
const POLL_INTERVAL: Duration = Duration::from_millis(500);
const MAX_ATTEMPTS: u32 = 120;
info!(session = %self.name, "waiting for sync session to reach watching state");
for attempt in 1..=MAX_ATTEMPTS {
match self.query_status()? {
Status::Watching => {
info!(session = %self.name, "sync session is watching");
return Ok(());
}
status if status.is_halted() => {
eyre::bail!(
"mutagen session '{}' halted with status {:?}",
self.name,
status,
);
}
status => {
debug!(
session = %self.name,
attempt,
?status,
"waiting…",
);
std::thread::sleep(POLL_INTERVAL);
}
}
}
eyre::bail!(
"mutagen session '{}' did not reach watching state within {}s",
self.name,
MAX_ATTEMPTS / 2,
)
}
fn query_status(&self) -> eyre::Result<Status> {
let output = cmd::list(&self.name)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eyre::bail!("mutagen sync list failed: {stderr}");
}
let entries: Vec<SessionEntry> = serde_json::from_slice(&output.stdout)
.map_err(|e| eyre::eyre!("failed to parse mutagen output: {e}"))?;
match entries.into_iter().next() {
Some(entry) => Ok(entry.status),
None => {
warn!(session = %self.name, "session not found in list output");
Ok(Status::Disconnected)
}
}
}
}
impl Drop for Session {
fn drop(&mut self) {
if let Err(e) = cmd::terminate(&self.name) {
warn!(session = %self.name, "failed to terminate mutagen session: {e}");
}
}
}
mod cmd {
use std::process::{Command, ExitStatus, Output};
pub(super) fn create(workspace: &str, target: &str, name: &str) -> std::io::Result<ExitStatus> {
Command::new("mutagen")
.args(["sync", "create", workspace, target, "--name", name])
.status()
}
pub(super) fn list(name: &str) -> std::io::Result<Output> {
Command::new("mutagen")
.args(["sync", "list", "--template", "{{ json . }}", name])
.output()
}
pub(super) fn terminate(name: &str) -> std::io::Result<ExitStatus> {
Command::new("mutagen")
.args(["sync", "terminate", name])
.status()
}
}