use std::{fs, net::SocketAddr, path::PathBuf, process::Stdio, time::Duration};
use anyhow::{Context, anyhow};
use holochain_client_instrumented::ToSocketAddr;
use holochain_conductor_api::{
AdminInterfaceConfig, InterfaceDriver,
conductor::{
ConductorConfig, KeystoreConfig,
paths::{ConfigFilePath, ConfigRootPath},
},
};
use holochain_conductor_config::config::write_config;
use holochain_types::websocket::AllowedOrigins;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::{Child, Command},
time::timeout,
};
use wind_tunnel_runner::prelude::WindTunnelResult;
#[derive(Debug, Default)]
pub struct HolochainConfigBuilder {
bin_path: Option<PathBuf>,
agent_name: Option<String>,
admin_port: Option<u16>,
conductor_root_path: Option<PathBuf>,
target_arc_factor: Option<u32>,
metrics_path: Option<PathBuf>,
}
impl HolochainConfigBuilder {
pub fn with_target_arc_factor(&mut self, target_arc_factor: u32) -> &mut Self {
self.target_arc_factor = Some(target_arc_factor);
self
}
pub(crate) fn with_bin_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.bin_path = Some(path.into());
self
}
pub(crate) fn with_admin_port(&mut self, port: u16) -> &mut Self {
self.admin_port = Some(port);
self
}
pub(crate) fn with_agent_name(&mut self, agent_name: impl Into<String>) -> &mut Self {
self.agent_name = Some(agent_name.into());
self
}
pub(crate) fn with_conductor_root_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.conductor_root_path = Some(path.into());
self
}
pub(crate) fn with_metrics_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.metrics_path = Some(path.into());
self
}
pub(crate) fn build(self) -> WindTunnelResult<HolochainConfig> {
let bin_path = self.bin_path.unwrap_or(PathBuf::from("holochain"));
let conductor_root_path = self.conductor_root_path.ok_or(anyhow!(
"Conductor root path not set, this should be set by the Wind Tunnel runner"
))?;
let keystore_path = conductor_root_path.clone().join("ks");
let mut conductor_config = if let Some(admin_port) = self.admin_port {
ConductorConfig {
data_root_path: Some(conductor_root_path.clone().into()),
admin_interfaces: Some(vec![AdminInterfaceConfig {
driver: InterfaceDriver::Websocket {
port: admin_port,
danger_bind_addr: None,
allowed_origins: AllowedOrigins::Any,
},
}]),
keystore: KeystoreConfig::LairServerInProc {
lair_root: Some(keystore_path.clone().into()),
},
..Default::default()
}
} else {
ConductorConfig::default()
};
if let Some(target_arc_factor) = self.target_arc_factor {
conductor_config.network.target_arc_factor = target_arc_factor;
}
let metrics_path = self.metrics_path.ok_or(anyhow!(
"Metrics path not set, this should be set by the Wind Tunnel runner"
))?;
Ok(HolochainConfig {
bin_path,
agent_name: self.agent_name,
conductor_root_path,
conductor_config,
metrics_path,
})
}
}
#[derive(Debug, Clone)]
pub struct HolochainConfig {
bin_path: PathBuf,
agent_name: Option<String>,
conductor_root_path: PathBuf,
conductor_config: ConductorConfig,
metrics_path: PathBuf,
}
#[derive(Debug)]
pub struct HolochainRunner {
config: HolochainConfig,
holochain_handle: Option<Child>,
admin_ws_url: Option<SocketAddr>,
}
impl HolochainRunner {
pub fn create(config: &HolochainConfig) -> WindTunnelResult<Self> {
fs::create_dir_all(&config.conductor_root_path).with_context(|| {
format!(
"Failed to create conductor root directory '{}'",
config.conductor_root_path.display()
)
})?;
let conductor_root_path = config.conductor_root_path.canonicalize()?;
log::trace!(
"Writing conductor config file to '{}'",
conductor_root_path.display()
);
write_config(
config.conductor_root_path.clone().into(),
&config.conductor_config,
)
.context("Failed to write conductor config file")?;
Ok(Self {
config: config.clone(),
holochain_handle: None,
admin_ws_url: None,
})
}
pub async fn run(&mut self) -> WindTunnelResult<()> {
let config_root_path: ConfigRootPath = self.config.conductor_root_path.clone().into();
let config_file_path: ConfigFilePath = config_root_path.into();
log::info!("Running a Holochain conductor");
let mut holochain_handle = Command::new(&self.config.bin_path)
.env("HOLOCHAIN_INFLUXIVE_FILE", self.config.metrics_path.clone())
.arg("--config-path")
.arg(config_file_path.as_os_str())
.arg("--piped")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.kill_on_drop(true)
.spawn()
.context("Failed to run Holochain conductor")?;
log::trace!("Passing password to running conductor");
holochain_handle
.stdin
.take()
.context("Failed to get stdin for the running Holochain conductor")?
.write_all(b"1234\n")
.await
.context("Failed to write the password to the process running the conductor")?;
log::trace!("Waiting for the conductor to start");
let holochain_stdout = holochain_handle
.stdout
.take()
.context("Failed to get stdout for the running Holochain conductor")?;
let agent_name = self.config.agent_name.clone();
timeout(Duration::from_secs(30), async move {
let mut stdout_lines = BufReader::new(holochain_stdout).lines();
loop {
let line = stdout_lines
.next_line()
.await
.context("Failed to read line from Holochain conductor stdout")?
.ok_or(anyhow!("Holochain conductor shutdown before it was ready"))?;
let mut log_target = "holochain_conductor".to_string();
if let Some(agent_name) = &agent_name {
log_target.push_str("::");
log_target.push_str(agent_name);
}
log::info!(target: &log_target, "{line}");
if line == "Conductor ready." {
tokio::spawn(async move {
while let Ok(Some(line)) = stdout_lines.next_line().await {
if log::log_enabled!(target: &log_target, log::Level::Info) {
log::info!(target: &log_target, "{line}");
}
}
});
return Ok::<(), anyhow::Error>(());
}
}
})
.await
.context("Timed-out whilst waiting for the Holochain conductor to be ready")??;
self.holochain_handle = Some(holochain_handle);
let admin_port = self
.admin_port()
.ok_or(anyhow::anyhow!("No admin interface in conductor config"))?;
self.admin_ws_url = Some(
format!("ws://127.0.0.1:{admin_port}")
.to_socket_addr()
.with_context(|| {
format!("Failed to create admin_ws_url from 'ws://127.0.0.1:{admin_port}'")
})?,
);
Ok(())
}
pub fn shutdown(&mut self) {
log::info!("Shutting down holochain conductor");
self.holochain_handle = None;
}
pub fn admin_ws_url(&self) -> Option<SocketAddr> {
self.admin_ws_url
}
fn admin_port(&self) -> Option<u16> {
if let Some(admin_interfaces) = self.config.conductor_config.admin_interfaces.clone()
&& let Some(first_admin_interface) = admin_interfaces.first()
{
return Some(first_admin_interface.driver.port());
}
None
}
}
impl Drop for HolochainRunner {
fn drop(&mut self) {
log::trace!("Cleaning up the conductor files");
if let Err(err) = fs::remove_dir_all(&self.config.conductor_root_path) {
log::error!("Failed to cleanup the conductor files: {err}");
} else {
log::info!("Successfully cleaned up the conductor files");
}
if let Some(parent) = self.config.conductor_root_path.parent()
&& fs::remove_dir(parent).is_ok()
{
log::info!("Successfully cleaned up all conductor directories");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn conductor_dir_retained_after_shutdown() {
let tmp = tempdir().unwrap();
let conductor_root = tmp.path().join("conductor");
let metrics_path = tmp.path().join("metrics.influx");
let mut builder = HolochainConfigBuilder::default();
builder
.with_conductor_root_path(&conductor_root)
.with_admin_port(0)
.with_agent_name("test-agent")
.with_metrics_path(&metrics_path);
let config = builder.build().expect("Failed to build HolochainConfig");
let mut runner = HolochainRunner::create(&config).expect("Failed to create runner");
assert!(conductor_root.exists(),);
runner.shutdown();
assert!(conductor_root.exists());
drop(runner);
assert!(!conductor_root.exists());
}
}