use crate::cmds::*;
use clap::{ArgAction, Parser};
use holochain_conductor_api::conductor::paths::ConfigRootPath;
use holochain_trace::Output;
use holochain_types::prelude::InstalledAppId;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::path::PathBuf;
const DEFAULT_APP_ID: &str = "test-app";
#[derive(Debug, Parser)]
#[command(version, about)]
pub struct HcSandbox {
#[command(subcommand)]
subcommand: HcSandboxSubcommand,
#[arg(long)]
piped: bool,
#[arg(long, default_value_t = Output::Log)]
structured: Output,
#[arg(short, long, value_delimiter = ',')]
force_admin_ports: Vec<u16>,
#[arg(
short = 'H',
long,
env = "HC_HOLOCHAIN_PATH",
default_value = "holochain"
)]
holochain_path: PathBuf,
}
#[derive(Debug, Parser)]
#[command(infer_subcommands = true)]
pub enum HcSandboxSubcommand {
Generate {
#[arg(short, long, default_value = DEFAULT_APP_ID)]
app_id: InstalledAppId,
#[command(flatten)]
create: Create,
#[arg(short, long, value_delimiter = ',')]
run: Option<Vec<u16>>,
happ: Option<PathBuf>,
#[arg(long, short = 's')]
network_seed: Option<String>,
#[arg(long)]
roles_settings: Option<PathBuf>,
},
Run(Run),
List {
#[arg(short, long, action = ArgAction::SetTrue)]
verbose: bool,
},
Clean,
Remove {
#[arg(required = true, num_args = 1..)]
indices: Vec<usize>,
},
AdminPorts {
#[command(flatten)]
existing: Existing,
},
Create(Create),
}
#[derive(Debug, Parser)]
pub struct Run {
#[arg(short, long, value_delimiter = ',')]
ports: Vec<u16>,
#[command(flatten)]
existing: Existing,
}
impl HcSandbox {
pub async fn run(self) -> anyhow::Result<()> {
holochain_util::pw::pw_set_piped(self.piped);
match self.subcommand {
HcSandboxSubcommand::Generate {
app_id,
create,
run,
happ,
network_seed,
roles_settings,
} => {
let paths = generate(
&self.holochain_path,
happ,
create,
app_id,
network_seed,
roles_settings,
self.structured.clone(),
)
.await?;
for (port, path) in self
.force_admin_ports
.clone()
.into_iter()
.zip(paths.clone().into_iter())
{
crate::force_admin_port(path, port)?;
}
if let Some(ports) = run {
let holochain_path = self.holochain_path.clone();
let force_admin_ports = self.force_admin_ports.clone();
let structured = self.structured.clone();
let result = tokio::select! {
result = tokio::signal::ctrl_c() => {
msg!("Received Ctrl-C, shutting down");
result.map_err(anyhow::Error::from)
}
result = run_n(&holochain_path, paths, ports, force_admin_ports, structured) => result,
};
crate::save::release_ports(std::env::current_dir()?).await?;
return result;
}
}
HcSandboxSubcommand::Run(Run { ports, existing }) => {
let paths = existing.load(std::env::current_dir()?)?;
if paths.is_empty() {
tracing::warn!("no paths available, exiting.");
return Ok(());
}
let holochain_path = self.holochain_path.clone();
let force_admin_ports = self.force_admin_ports.clone();
let result = tokio::select! {
result = tokio::signal::ctrl_c() => {
msg!("Received Ctrl-C, shutting down");
result.map_err(anyhow::Error::from)
}
result = run_n(&holochain_path, paths.into_iter().map(ConfigRootPath::from).collect(), ports, force_admin_ports, self.structured) => result,
};
crate::save::release_ports(std::env::current_dir()?).await?;
return result;
}
HcSandboxSubcommand::List { verbose } => {
crate::save::list(std::env::current_dir()?, verbose)?
}
HcSandboxSubcommand::Clean => {
let removed_count = crate::save::remove(
std::env::current_dir()?,
Existing {
all: true,
indices: vec![],
},
)?;
match removed_count {
0 => msg!("No sandbox paths have been removed"),
1 => msg!("1 sandbox path has been removed"),
_ => msg!("{} sandbox paths have been removed", removed_count),
}
}
HcSandboxSubcommand::Remove { indices } => {
let removed_count = crate::save::remove(
std::env::current_dir()?,
Existing {
all: false,
indices,
},
)?;
match removed_count {
0 => msg!("No sandbox path has been removed"),
1 => msg!("1 sandbox path has been removed"),
_ => msg!("{} sandbox paths have been removed", removed_count),
}
}
HcSandboxSubcommand::AdminPorts { existing } => {
let paths = existing.load(std::env::current_dir()?)?;
if paths.is_empty() {
println!("[]");
return Ok(());
}
let ports = crate::ports::get_admin_ports(paths).await?;
println!("{}", serde_json::to_string(&ports)?);
}
HcSandboxSubcommand::Create(Create {
num_sandboxes,
network,
root,
directories,
in_process_lair,
#[cfg(feature = "chc")]
chc_url,
}) => {
let mut paths = Vec::with_capacity(num_sandboxes.into());
match num_sandboxes.into() {
1 => msg!("Creating 1 conductor sandbox"),
_ => msg!(
"Creating {} conductor sandboxes with same settings",
num_sandboxes
),
}
for i in 0..num_sandboxes.into() {
let network = Network::to_kitsune(&NetworkCmd::as_inner(&network)).await;
let path = holochain_conductor_config::generate::generate(
network,
root.clone(),
directories.get(i).cloned(),
in_process_lair,
0,
#[cfg(feature = "chc")]
chc_url.clone(),
)?;
paths.push(path);
}
save_and_print(std::env::current_dir()?, paths)?;
}
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LaunchInfo {
pub admin_port: u16,
pub app_ports: Vec<u16>,
}
impl LaunchInfo {
pub(crate) fn from_admin_port(admin_port: u16) -> Self {
LaunchInfo {
admin_port,
app_ports: vec![],
}
}
}
pub async fn run_n(
holochain_path: &Path,
paths: Vec<ConfigRootPath>,
app_ports: Vec<u16>,
force_admin_ports: Vec<u16>,
structured: Output,
) -> anyhow::Result<()> {
let run_holochain = |holochain_path: PathBuf,
path: ConfigRootPath,
index: usize,
ports,
force_admin_port,
structured| async move {
crate::run::run(
&holochain_path,
path,
index,
ports,
force_admin_port,
structured,
)
.await?;
Result::<_, anyhow::Error>::Ok(())
};
let mut force_admin_ports = force_admin_ports.into_iter();
let mut app_ports = app_ports.into_iter();
let jhs = paths
.into_iter()
.enumerate()
.zip(std::iter::repeat_with(|| force_admin_ports.next()))
.zip(std::iter::repeat_with(|| app_ports.next()))
.map(|(((index, path), force_admin_port), app_port)| {
let f = run_holochain(
holochain_path.to_path_buf(),
path,
index,
app_port.map(|p| vec![p]).unwrap_or_default(),
force_admin_port,
structured.clone(),
);
tokio::task::spawn(f)
});
futures::future::try_join_all(jhs).await?;
Ok(())
}
pub async fn generate(
holochain_path: &Path,
happ: Option<PathBuf>,
create: Create,
app_id: InstalledAppId,
network_seed: Option<String>,
roles_settings: Option<PathBuf>,
structured: Output,
) -> anyhow::Result<Vec<ConfigRootPath>> {
let happ = crate::bundles::parse_happ(happ)?;
match create.num_sandboxes.into() {
1 => msg!("Creating 1 conductor sandbox"),
_ => msg!(
"Creating {} conductor sandboxes with same settings",
create.num_sandboxes
),
}
let paths = crate::sandbox::default_n(
holochain_path,
create,
happ,
app_id,
network_seed,
roles_settings,
structured,
)
.await?;
save_and_print(std::env::current_dir()?, paths.clone())?;
Ok(paths)
}
fn save_and_print(hc_dir: PathBuf, paths: Vec<ConfigRootPath>) -> std::io::Result<()> {
let pre_existing = crate::save::load(hc_dir.clone())?;
crate::save::save(hc_dir, paths.clone())?;
match paths.len() {
1 => msg!(
"Created 1 sandbox:\n{}:{}",
pre_existing.len(),
paths[0].display()
),
_ => {
msg!("Created {} sandboxes:", paths.len());
for (i, path) in paths.into_iter().enumerate() {
msg!("{}:{}", pre_existing.len() + i, path.display());
}
}
}
Ok(())
}