use crate::bin_path::{WT_HOLOCHAIN_PATH_ENV, holochain_path};
use crate::build_info::holochain_build_info;
use crate::context::HolochainAgentContext;
use crate::holochain_runner::{HolochainConfig, HolochainRunner};
use crate::prelude::CallZomeOptions;
use crate::runner_context::HolochainRunnerContext;
use anyhow::Context;
use holochain_client_instrumented::ToSocketAddr;
use holochain_client_instrumented::prelude::{
AdminWebsocket, AppWebsocket, AuthorizeSigningCredentialsPayload, ClientAgentSigner,
};
use holochain_conductor_api::{AppInfo, CellInfo};
use holochain_types::prelude::*;
use holochain_types::prelude::{
AppBundleSource, CellId, ExternIO, InstallAppPayload, InstalledAppId, RoleName,
};
use holochain_types::websocket::AllowedOrigins;
use kitsune2_api::{AgentInfoSigned, DhtArc};
use kitsune2_core::Ed25519Verifier;
use rand::rng;
use rand::seq::SliceRandom;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{env, fs, io};
use wind_tunnel_runner::prelude::{
AgentContext, HookResult, Reporter, RunnerContext, UserValuesConstraint, WindTunnelResult,
};
use wind_tunnel_summary_model::BuildInfo;
pub fn configure_admin_ws_url<SV: UserValuesConstraint>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> WindTunnelResult<()> {
if ctx.get().admin_ws_url.is_none() {
ctx.get_mut().admin_ws_url = Some(
ctx.runner_context()
.get_connection_string()
.context("Need to call run_holochain_conductor in agent_setup or set connection-string so an admin_port can be established")?
.to_socket_addr()
.context("Failed to convert connection-string to admin_ws_url")?,
);
}
Ok(())
}
pub fn configure_app_ws_url<SV: UserValuesConstraint>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> WindTunnelResult<()> {
let admin_ws_url = ctx.get().admin_ws_url();
let reporter = ctx.runner_context().reporter();
let app_port = ctx
.runner_context()
.executor()
.execute_in_place(async move {
log::debug!("Connecting a Holochain admin client: {admin_ws_url}");
let admin_client = AdminWebsocket::connect(admin_ws_url, None, reporter)
.await
.context("Unable to connect admin client")?;
let existing_app_interfaces = admin_client.list_app_interfaces().await?;
let existing_app_ports = existing_app_interfaces
.into_iter()
.filter_map(|interface| {
if interface.allowed_origins == AllowedOrigins::Any
&& interface.installed_app_id.is_none()
{
Some(interface.port)
} else {
None
}
})
.collect::<Vec<_>>();
if !existing_app_ports.is_empty() {
Ok(*existing_app_ports.first().context("No app ports found")?)
} else {
let attached_app_port = admin_client
.attach_app_interface(0, None, AllowedOrigins::Any, None)
.await?;
Ok(attached_app_port)
}
})
.context("Failed to set up app port, is a conductor running?")?;
let mut app_ws_url = admin_ws_url;
app_ws_url.set_port(app_port);
ctx.get_mut().app_ws_url = Some(app_ws_url);
Ok(())
}
pub fn install_app<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
app_path: PathBuf,
role_name: &RoleName,
) -> WindTunnelResult<()>
where
SV: UserValuesConstraint,
{
install_app_custom(ctx, app_path, role_name, None, None)
}
pub fn install_app_custom<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
app_path: PathBuf,
role_name: &RoleName,
agent_pubkey: Option<AgentPubKey>,
roles_settings: Option<HashMap<String, RoleSettings>>,
) -> WindTunnelResult<()>
where
SV: UserValuesConstraint,
{
let admin_ws_url = ctx.get().admin_ws_url();
let app_ws_url = ctx.get().app_ws_url();
let installed_app_id = installed_app_id_for_agent(ctx);
let reporter = ctx.runner_context().reporter();
let run_id = ctx.runner_context().get_run_id().to_string();
let (installed_app_id, cell_id, app_client) = ctx
.runner_context()
.executor()
.execute_in_place(async move {
log::debug!("Connecting a Holochain admin client: {admin_ws_url}");
let client = AdminWebsocket::connect(admin_ws_url, None, reporter.clone()).await?;
let key = match agent_pubkey {
Some(key) => {
log::debug!("Using provided agent pub key: {:}", key);
key
}
None => {
let key = client.generate_agent_pub_key().await?;
log::debug!("Generated agent pub key: {:}", key);
key
}
};
let content = std::fs::read(app_path)?;
let app_info = client
.install_app(InstallAppPayload {
source: AppBundleSource::Bytes(bytes::Bytes::from(content)),
agent_key: Some(key),
installed_app_id: Some(installed_app_id.clone()),
roles_settings,
network_seed: Some(run_id),
ignore_genesis_failure: false,
})
.await?;
log::debug!("Installed app: {installed_app_id}");
client.enable_app(installed_app_id.clone()).await?;
log::debug!("Enabled app: {installed_app_id}");
let cell_id = get_cell_id_for_role_name(&app_info, role_name)?;
log::debug!("Got cell id: {cell_id:?}");
let credentials = client
.authorize_signing_credentials(AuthorizeSigningCredentialsPayload {
cell_id: cell_id.clone(),
functions: None,
})
.await?;
log::debug!("Authorized signing credentials");
let signer = ClientAgentSigner::default();
signer.add_credentials(cell_id.clone(), credentials);
let issued = client
.issue_app_auth_token(installed_app_id.clone().into())
.await
.map_err(|e| anyhow::anyhow!("Could not issue auth token for app client: {e:?}"))?;
let app_client =
AppWebsocket::connect(app_ws_url, issued.token, signer.into(), None, reporter)
.await?;
Ok((installed_app_id, cell_id, app_client))
})
.context("Failed to install app")?;
ctx.get_mut().installed_app_id = Some(installed_app_id);
ctx.get_mut().cell_role_name = Some(role_name.clone());
ctx.get_mut().cell_id = Some(cell_id);
ctx.get_mut().app_client = Some(app_client);
Ok(())
}
pub fn use_installed_app<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
role_name: &RoleName,
) -> HookResult
where
SV: UserValuesConstraint,
{
let admin_ws_url = ctx.get().admin_ws_url();
let app_ws_url = ctx.get().app_ws_url();
let reporter = ctx.runner_context().reporter();
let installed_app_id = installed_app_id_for_agent(ctx);
let (installed_app_id, cell_id, app_client) = ctx
.runner_context()
.executor()
.execute_in_place(async move {
let client = AdminWebsocket::connect(admin_ws_url, None, reporter.clone()).await?;
let app_infos = client.list_apps(None).await?;
let app_info = app_infos
.into_iter()
.find(|app_info| app_info.installed_app_id == installed_app_id)
.ok_or(anyhow::anyhow!("App not found: {installed_app_id:?}"))?;
if app_info.status != AppStatus::Enabled {
anyhow::bail!("App is not enabled: {installed_app_id:?}");
}
let cell_id = get_cell_id_for_role_name(&app_info, role_name)?;
let credentials = client
.authorize_signing_credentials(AuthorizeSigningCredentialsPayload {
cell_id: cell_id.clone(),
functions: None,
})
.await?;
let signer = ClientAgentSigner::default();
signer.add_credentials(cell_id.clone(), credentials);
let issued = client
.issue_app_auth_token(installed_app_id.clone().into())
.await
.map_err(|e| anyhow::anyhow!("Could not issue auth token for app client: {e:?}"))?;
let app_client =
AppWebsocket::connect(app_ws_url, issued.token, signer.into(), None, reporter)
.await?;
Ok((installed_app_id, cell_id, app_client))
})?;
ctx.get_mut().installed_app_id = Some(installed_app_id);
ctx.get_mut().cell_role_name = Some(role_name.clone());
ctx.get_mut().cell_id = Some(cell_id);
ctx.get_mut().app_client = Some(app_client);
Ok(())
}
pub fn try_wait_for_min_agents<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
wait_for: Duration,
) -> HookResult
where
SV: UserValuesConstraint,
{
let admin_ws_url = ctx.get().admin_ws_url();
let reporter = ctx.runner_context().reporter();
let agent_name = ctx.agent_name().to_string();
let min_agents = std::env::var("MIN_AGENTS")
.ok()
.map(|s| s.parse().expect("MIN_AGENTS must be a number"))
.unwrap_or(2);
ctx.runner_context()
.executor()
.execute_in_place(async move {
let client = AdminWebsocket::connect(admin_ws_url, None, reporter.clone()).await?;
let start_discovery = Instant::now();
for _ in 0..wait_for.as_secs() {
let agent_list = client.agent_info(None).await?;
if agent_list.len() >= min_agents {
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
println!(
"Discovery for agent {} took: {}s",
agent_name,
start_discovery.elapsed().as_secs()
);
Ok(())
})?;
Ok(())
}
pub fn try_wait_until_full_arc_peer_discovered<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> HookResult
where
SV: UserValuesConstraint,
{
let start_discovery = Instant::now();
loop {
if ctx.shutdown_listener().should_shutdown() {
break;
}
let app_client = ctx.get().app_client();
let full_arc_node_discovered =
ctx.runner_context()
.executor()
.execute_in_place(async move {
let agent_infos_encoded = app_client.agent_info(None).await?;
let full_arc_nodes: Vec<Arc<AgentInfoSigned>> = agent_infos_encoded
.iter()
.filter_map(|agent_info| {
AgentInfoSigned::decode(&Ed25519Verifier, agent_info.as_bytes()).ok()
})
.filter(|agent_info| agent_info.storage_arc == DhtArc::FULL)
.collect();
if !full_arc_nodes.is_empty() {
return Ok(true);
}
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(false)
})?;
if full_arc_node_discovered {
ctx.runner_context()
.executor()
.execute_in_place(async move {
tokio::time::sleep(Duration::from_secs(5)).await;
Ok(())
})?;
break;
}
}
println!(
"Discovery of full arc took: {}s",
start_discovery.elapsed().as_secs()
);
Ok(())
}
pub fn uninstall_app<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
installed_app_id: Option<InstalledAppId>,
) -> HookResult
where
SV: UserValuesConstraint,
{
let admin_ws_url = ctx.get().admin_ws_url();
let installed_app_id = installed_app_id.or_else(|| ctx.get().installed_app_id().ok());
if installed_app_id.is_none() {
log::info!("No installed app id found, skipping uninstall");
return Ok(());
}
let reporter = ctx.runner_context().reporter();
ctx.runner_context()
.executor()
.execute_in_place(async move {
let admin_client = AdminWebsocket::connect(admin_ws_url, None, reporter).await?;
admin_client
.uninstall_app(installed_app_id.unwrap())
.await?;
Ok(())
})?;
Ok(())
}
pub fn call_zome<I, O, SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
zome_name: &str,
fn_name: &str,
payload: I,
) -> anyhow::Result<O>
where
O: std::fmt::Debug + serde::de::DeserializeOwned,
I: serde::Serialize + std::fmt::Debug,
SV: UserValuesConstraint,
{
call_zome_with_options(ctx, zome_name, fn_name, payload, CallZomeOptions::default())
}
pub fn call_zome_with_options<I, O, SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
zome_name: &str,
fn_name: &str,
payload: I,
options: CallZomeOptions,
) -> anyhow::Result<O>
where
O: std::fmt::Debug + serde::de::DeserializeOwned,
I: serde::Serialize + std::fmt::Debug,
SV: UserValuesConstraint,
{
let cell_id = ctx.get().cell_id();
let app_agent_client = ctx.get().app_client();
ctx.runner_context().executor().execute_in_place(async {
let result = app_agent_client
.call_zome(
cell_id.into(),
zome_name,
fn_name,
ExternIO::encode(payload).context("Encoding failure")?,
options,
)
.await?;
result
.decode()
.map_err(|e| anyhow::anyhow!("Decoding failure: {e:?}"))
})
}
pub fn get_peer_list_randomized<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> WindTunnelResult<Vec<AgentPubKey>>
where
SV: UserValuesConstraint,
{
let cell_id = ctx.get().cell_id();
let reporter: Arc<Reporter> = ctx.runner_context().reporter();
let admin_ws_url = ctx.get().admin_ws_url();
ctx.runner_context()
.executor()
.execute_in_place(async move {
let admin_client = AdminWebsocket::connect(admin_ws_url, None, reporter).await?;
let agent_infos_encoded = admin_client
.agent_info(None)
.await
.context("Failed to get agent info")?;
let mut peer_list = Vec::with_capacity(agent_infos_encoded.len().saturating_sub(1));
for info_encoded in agent_infos_encoded {
let info_decoded =
AgentInfoSigned::decode(&Ed25519Verifier, info_encoded.as_bytes())?;
let agent_pub_key = AgentPubKey::from_k2_agent(&info_decoded.agent);
if &agent_pub_key != cell_id.agent_pubkey() {
peer_list.push(agent_pub_key);
}
}
peer_list.shuffle(&mut rng());
Ok(peer_list)
})
}
fn installed_app_id_for_agent<SV>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> String
where
SV: UserValuesConstraint,
{
let agent_name = ctx.agent_name().to_string();
format!("{agent_name}-app").to_string()
}
fn get_cell_id_for_role_name(app_info: &AppInfo, role_name: &RoleName) -> anyhow::Result<CellId> {
match app_info
.cell_info
.get(role_name)
.ok_or(anyhow::anyhow!("Role not found"))?
.first()
.ok_or(anyhow::anyhow!("Cell not found"))?
{
CellInfo::Provisioned(c) => Ok(c.cell_id.clone()),
_ => anyhow::bail!("Cell not provisioned"),
}
}
pub fn run_holochain_conductor<SV: UserValuesConstraint>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> WindTunnelResult<()> {
if ctx.runner_context().get_connection_string().is_some() {
log::info!(
"connection-string is set so assuming a Holochain conductor is running externally"
);
return Ok(());
}
let holochain_path = holochain_path()?;
log::debug!(
"Using holochain binary at path: {}",
holochain_path.display()
);
ctx.get_mut()
.holochain_config_mut()
.with_bin_path(holochain_path);
let admin_port = {
let listener = std::net::TcpListener::bind(("127.0.0.1", 0))
.context("Failed to bind ephemeral port for admin interface")?;
listener.local_addr()?.port()
};
let conductor_root_path = {
let mut path = env::temp_dir();
path.push(ctx.runner_context().get_run_id());
path.push(ctx.agent_name());
path
};
let holochain_metrics_path = {
let mut path: PathBuf = std::env::var("WT_METRICS_DIR")
.expect("WT_METRICS_DIR must be set.")
.into();
path.push(format!(
"holochain-{}-{}.influx",
ctx.runner_context().get_run_id(),
ctx.agent_name()
));
path
};
let agent_name = ctx.agent_name().to_string();
ctx.get_mut()
.holochain_config_mut()
.with_conductor_root_path(&conductor_root_path)
.with_admin_port(admin_port)
.with_agent_name(agent_name)
.with_metrics_path(&holochain_metrics_path);
let config = ctx.get_mut().take_holochain_config().build()?;
ctx.get_mut().holochain_runner = match ctx.runner_context().executor().execute_in_place(
create_and_start_holochain_conductor(config, &conductor_root_path),
) {
Ok(runner) => Some(runner),
Err(err) => {
log::error!("Failed to start Holochain conductor: {err}");
ctx.runner_context().force_stop_scenario();
return Err(err);
}
};
ctx.get_mut().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(())
}
async fn create_and_start_holochain_conductor(
config: HolochainConfig,
conductor_root_path: &Path,
) -> anyhow::Result<HolochainRunner> {
let mut err = match async {
let mut runner = HolochainRunner::create(&config)?;
log::info!("Created runner {runner:?}");
runner.run().await?;
anyhow::Ok(runner)
}
.await
{
Ok(runner) => return Ok(runner),
Err(err) => err,
};
log::trace!("Error whilst starting conductor so cleaning up directory");
if let Err(err) = fs::remove_dir_all(conductor_root_path) {
log::error!("Failed to cleanup the conductor files: {err}");
} else {
log::info!("Successfully cleaned up the conductor files after error");
}
if let Some(parent) = conductor_root_path.parent()
&& fs::remove_dir(parent).is_ok()
{
log::info!("Successfully cleaned up all conductor directories after error");
}
if let Some(io_error) = err.root_cause().downcast_ref::<io::Error>()
&& io_error.kind() == io::ErrorKind::NotFound
{
if let Err(_) | Ok("holochain") = env::var(WT_HOLOCHAIN_PATH_ENV).as_deref() {
err = err.context("'holochain' binary not found in your PATH");
} else {
err = err.context(format!("Cannot run 'holochain' binary found at the path provided with '{WT_HOLOCHAIN_PATH_ENV}'"));
}
}
Err(err)
}
pub fn start_conductor_and_configure_urls<SV: UserValuesConstraint>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> WindTunnelResult<()> {
run_holochain_conductor(ctx)?;
configure_admin_ws_url(ctx)?;
configure_app_ws_url(ctx)
}
pub fn conductor_build_info(
runner_ctx: Arc<RunnerContext<HolochainRunnerContext>>,
) -> WindTunnelResult<Option<BuildInfo>> {
if runner_ctx.get_connection_string().is_some() {
log::info!(
"connection-string is set so assuming a Holochain conductor is running externally"
);
return Ok(None);
}
let holochain_path = holochain_path()?;
let holochain_build_info = holochain_build_info(holochain_path)?;
let build_info = BuildInfo {
info_type: "holochain".to_string(),
info: serde_json::to_value(holochain_build_info)?,
};
Ok(Some(build_info))
}
pub fn stop_holochain_conductor<SV: UserValuesConstraint>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> WindTunnelResult<()> {
if let Some(mut runner) = ctx.get_mut().holochain_runner.take() {
log::info!("Stopping Holochain conductor");
runner.shutdown();
log::info!("Holochain conductor stopped");
let ctx = ctx.get_mut();
ctx.holochain_runner = Some(runner);
ctx.app_client = None;
ctx.app_ws_url = None;
ctx.admin_ws_url = None;
} else {
log::debug!("No Holochain conductor is running, so nothing to stop");
}
Ok(())
}
pub fn start_holochain_conductor<SV: UserValuesConstraint>(
ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<SV>>,
) -> WindTunnelResult<()> {
if let Some(mut runner) = ctx.get_mut().holochain_runner.take() {
log::info!("Starting Holochain conductor");
if let Err(err) = ctx
.runner_context()
.executor()
.execute_in_place(runner.run())
{
log::error!("Failed to start Holochain conductor: {err}");
ctx.runner_context().force_stop_scenario();
return Err(err);
}
log::info!("Holochain conductor started");
let admin_ws_url = runner.admin_ws_url().ok_or(anyhow::anyhow!(
"Failed to get admin websocket url of Holochain conductor"
))?;
ctx.get_mut().holochain_runner = Some(runner);
ctx.get_mut().admin_ws_url = Some(admin_ws_url);
configure_app_ws_url(ctx)?;
let cell_role_name = ctx.get().cell_role_name();
use_installed_app(ctx, &cell_role_name)?;
} else {
log::debug!(
"No Holochain runner in context, did you forget to call create_and_start_holochain_conductor?"
);
}
Ok(())
}