use clap::Parser;
use holochain_conductor_api::conductor::NetworkConfig;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use url2::Url2;
#[derive(Debug, Parser, Clone)]
pub struct Create {
#[arg(short, long, default_value = "1")]
pub num_sandboxes: NonZeroUsize,
#[command(subcommand)]
pub network: Option<NetworkCmd>,
#[arg(long)]
pub root: Option<PathBuf>,
#[arg(short, long, value_delimiter = ',')]
pub directories: Vec<PathBuf>,
#[arg(long)]
pub in_process_lair: bool,
#[cfg(feature = "chc")]
#[arg(long, value_parser=try_parse_url2)]
pub chc_url: Option<Url2>,
}
#[derive(Debug, Parser, Clone)]
pub enum NetworkCmd {
Network(Network),
}
impl NetworkCmd {
pub fn as_inner(this: &Option<Self>) -> Option<&Network> {
match this {
None => None,
Some(NetworkCmd::Network(n)) => Some(n),
}
}
}
#[derive(Debug, Parser, Clone)]
pub struct Network {
#[command(subcommand)]
pub transport: NetworkType,
#[arg(short, long, value_parser = try_parse_url2)]
pub bootstrap: Option<Url2>,
#[arg(long)]
pub target_arc_factor: Option<u32>,
}
#[derive(Debug, Parser, Clone)]
pub enum NetworkType {
Mem,
#[cfg(feature = "transport-tx5-backend-go-pion")]
#[command(name = "webrtc")]
WebRTC {
#[arg(value_parser = try_parse_url2)]
signal_url: Url2,
webrtc_config: Option<std::path::PathBuf>,
},
#[cfg(feature = "transport-iroh")]
#[command(name = "quic")]
QUIC {
#[arg(value_parser = try_parse_url2)]
relay_url: Url2,
},
}
#[derive(Debug, Parser, Clone)]
pub struct Existing {
#[arg(short, long, conflicts_with = "indices")]
pub all: bool,
#[arg(conflicts_with = "all")]
pub indices: Vec<usize>,
}
impl Existing {
pub fn load(self, hc_dir: PathBuf) -> std::io::Result<Vec<PathBuf>> {
let sandboxes = crate::save::load(hc_dir)?;
if sandboxes.is_empty() {
msg!(
"
Before running or calling you need to generate a sandbox.
You can use `hc sandbox generate` or `hc sandbox create` to do this.
Run `hc sandbox generate --help` or `hc sandbox create --help` for more options."
);
Err(std::io::Error::other("No sandboxes found."))
} else if self.all {
sandboxes
.iter()
.enumerate()
.filter_map(|(i, result)| result.as_ref().err().map(|path| (i, path)))
.for_each(|(i, path)| {
msg!(
"Missing sandbox: {}:{}",
i,
path.as_path().to_string_lossy()
)
});
Ok(sandboxes.into_iter().flatten().collect())
} else if !self.indices.is_empty() {
let mut set = std::collections::BTreeSet::new();
let dedup_indices = self
.indices
.into_iter()
.filter(|x| set.insert(*x))
.collect::<Vec<_>>();
let mut selected = Vec::with_capacity(dedup_indices.len());
for i in dedup_indices.into_iter() {
let Some(result) = sandboxes.get(i) else {
return Err(std::io::Error::other(format!(
"Index {i} is out of bounds."
)));
};
match result {
Err(path) => {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Missing sandbox {}:{}", i, path.as_path().to_string_lossy()),
))
}
Ok(path) => selected.push(path.clone()),
}
}
Ok(selected)
} else if sandboxes.len() == 1 {
match &sandboxes[0] {
Err(path) => {
msg!("Missing sandbox {}:{}", 0, path.as_path().to_string_lossy());
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Missing sandbox {}:{}", 0, path.as_path().to_string_lossy()),
))
}
Ok(path) => Ok(vec![path.clone()]),
}
} else {
msg!(
"
There are multiple sandboxes and hc doesn't know which one to run.
You can run:
- `--all` `-a` run all sandboxes.
- `1` run a sandbox by index from the list below.
- `0 2` run multiple sandboxes by indices from the list below.
Run `hc sandbox list` to see the sandboxes or `hc sandbox run --help` for more information."
);
crate::save::list(std::env::current_dir()?, false)?;
Err(std::io::Error::other(
"Multiple sandboxes found, please specify which to run.",
))
}
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty() && !self.all
}
}
impl Network {
pub async fn to_kitsune(this: &Option<&Self>) -> Option<NetworkConfig> {
let Network {
transport,
bootstrap,
target_arc_factor,
} = match this {
None => {
return Some(NetworkConfig {
advanced: Some(serde_json::json!({
"tx5Transport": {
"signalAllowPlainText": true,
},
"irohTransport": {
"relayAllowPlainText": true,
}
})),
..NetworkConfig::default()
});
}
Some(n) => (*n).clone(),
};
let mut network_config = NetworkConfig::default();
if let Some(bootstrap) = bootstrap {
network_config.bootstrap_url = bootstrap.to_owned();
}
if let Some(target_arc_factor) = target_arc_factor {
network_config.target_arc_factor = target_arc_factor.to_owned();
}
match transport {
NetworkType::Mem => (),
#[cfg(feature = "transport-tx5-backend-go-pion")]
NetworkType::WebRTC {
signal_url,
webrtc_config,
} => {
let webrtc_config = match webrtc_config {
Some(path) => {
let content = tokio::fs::read_to_string(path)
.await
.expect("failed to read webrtc_config file");
let parsed = serde_json::from_str(&content)
.expect("failed to parse webrtc_config file content");
Some(parsed)
}
None => None,
};
network_config.signal_url = signal_url;
network_config.webrtc_config = webrtc_config;
network_config.advanced = Some(serde_json::json!({
"tx5Transport": {
"signalAllowPlainText": true,
}
}));
}
#[cfg(feature = "transport-iroh")]
NetworkType::QUIC { relay_url } => {
network_config.relay_url = relay_url;
network_config.advanced = Some(serde_json::json!({
"irohTransport": {
"relayAllowPlainText": true,
}
}));
}
}
Some(network_config)
}
}
impl Default for Create {
fn default() -> Self {
Self {
num_sandboxes: NonZeroUsize::new(1).unwrap(),
network: None,
root: None,
directories: Vec::with_capacity(0),
in_process_lair: false,
#[cfg(feature = "chc")]
chc_url: None,
}
}
}
fn try_parse_url2(arg: &str) -> url2::Url2Result<Url2> {
Url2::try_parse(arg)
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow;
use holochain_conductor_api::conductor::paths::{ConfigFilePath, ConfigRootPath};
use std::fs;
use tempfile;
#[test]
fn test_existing_is_empty() {
let existing = Existing {
all: false,
indices: vec![],
};
assert!(existing.is_empty());
let existing = Existing {
all: true,
indices: vec![],
};
assert!(!existing.is_empty());
let existing = Existing {
all: false,
indices: vec![0],
};
assert!(!existing.is_empty());
}
#[test]
fn test_existing_load_invalid_path() -> anyhow::Result<()> {
let result = Existing {
all: true,
indices: vec![],
}
.load(PathBuf::from("invalid_path"));
assert!(result.is_err());
Ok(())
}
#[test]
fn test_existing_load_no_conductor_config_file() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let result = Existing {
all: true,
indices: vec![],
}
.load(test_dir.to_path_buf());
assert!(result.is_err());
Ok(())
}
#[test]
fn test_existing_load_empty_file() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
crate::save::save(test_dir.to_path_buf(), vec![])?;
let result = Existing {
all: true,
indices: vec![],
}
.load(test_dir.to_path_buf());
assert!(result.is_err());
Ok(())
}
#[test]
fn test_existing_load_all_valid_sandboxes() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
let sandbox2 = test_dir.join("sandbox2");
fs::create_dir_all(&sandbox1)?;
fs::create_dir_all(&sandbox2)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
let config_path2 = ConfigRootPath::from(sandbox2.clone());
let config_file_path2 = ConfigFilePath::from(config_path2.clone());
fs::create_dir_all(config_file_path2.as_ref().parent().unwrap())?;
fs::write(config_file_path2.as_ref(), "dummy config")?;
crate::save::save(test_dir.to_path_buf(), vec![config_path1, config_path2])?;
let paths = Existing {
all: true,
indices: vec![],
}
.load(test_dir.to_path_buf())?;
assert_eq!(paths.len(), 2);
assert_eq!(paths[0], sandbox1);
assert_eq!(paths[1], sandbox2);
Ok(())
}
#[test]
fn test_existing_load_all_with_invalid_sandboxes() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
let sandbox2 = test_dir.join("sandbox2");
fs::create_dir_all(&sandbox1)?;
fs::create_dir_all(&sandbox2)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
let config_path2 = ConfigRootPath::from(sandbox2.clone());
crate::save::save(test_dir.to_path_buf(), vec![config_path1, config_path2])?;
let existing = Existing {
all: true,
indices: vec![],
};
let paths = existing.load(test_dir.to_path_buf())?;
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], sandbox1);
Ok(())
}
#[test]
fn test_existing_load_specific_indices() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
let sandbox2 = test_dir.join("sandbox2");
let sandbox3 = test_dir.join("sandbox3");
fs::create_dir_all(&sandbox1)?;
fs::create_dir_all(&sandbox2)?;
fs::create_dir_all(&sandbox3)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
let config_path2 = ConfigRootPath::from(sandbox2.clone());
let config_file_path2 = ConfigFilePath::from(config_path2.clone());
fs::create_dir_all(config_file_path2.as_ref().parent().unwrap())?;
fs::write(config_file_path2.as_ref(), "dummy config")?;
let config_path3 = ConfigRootPath::from(sandbox3.clone());
let config_file_path3 = ConfigFilePath::from(config_path3.clone());
fs::create_dir_all(config_file_path3.as_ref().parent().unwrap())?;
fs::write(config_file_path3.as_ref(), "dummy config")?;
crate::save::save(
test_dir.to_path_buf(),
vec![config_path1, config_path2, config_path3],
)?;
let existing = Existing {
all: false,
indices: vec![0, 2],
};
let paths = existing.load(test_dir.to_path_buf())?;
assert_eq!(paths.len(), 2);
assert_eq!(paths[0], sandbox1);
assert_eq!(paths[1], sandbox3);
let existing = Existing {
all: false,
indices: vec![1],
};
let paths = existing.load(test_dir.to_path_buf())?;
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], sandbox2);
Ok(())
}
#[test]
fn test_existing_load_specific_indices_with_invalid_sandboxes() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
let sandbox2 = test_dir.join("sandbox2");
fs::create_dir_all(&sandbox1)?;
fs::create_dir_all(&sandbox2)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
let config_path2 = ConfigRootPath::from(sandbox2.clone());
crate::save::save(test_dir.to_path_buf(), vec![config_path1, config_path2])?;
let existing = Existing {
all: false,
indices: vec![0, 1],
};
let result = existing.load(test_dir.to_path_buf());
assert!(result.is_err());
Ok(())
}
#[test]
fn test_existing_load_duplicate_indices() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
fs::create_dir_all(&sandbox1)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
crate::save::save(
test_dir.to_path_buf(),
vec![config_path1.clone(), config_path1],
)?;
let existing = Existing {
all: false,
indices: vec![0, 0],
};
let result = existing.load(test_dir.to_path_buf())?;
assert_eq!(result.len(), 1);
let existing = Existing {
all: false,
indices: vec![0, 1, 0],
};
let result = existing.load(test_dir.to_path_buf())?;
assert_eq!(result.len(), 2);
Ok(())
}
#[test]
fn test_existing_load_out_of_range_indices() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
fs::create_dir_all(&sandbox1)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
crate::save::save(test_dir.to_path_buf(), vec![config_path1])?;
let existing = Existing {
all: false,
indices: vec![1], };
let result = existing.load(test_dir.to_path_buf());
assert!(result.is_err());
Ok(())
}
#[test]
fn test_existing_load_single_sandbox() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
fs::create_dir_all(&sandbox1)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
crate::save::save(test_dir.to_path_buf(), vec![config_path1])?;
let existing = Existing {
all: false,
indices: vec![],
};
let paths = existing.load(test_dir.to_path_buf())?;
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], sandbox1);
Ok(())
}
#[test]
fn test_existing_load_single_invalid_sandbox() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
fs::create_dir_all(&sandbox1)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
crate::save::save(test_dir.to_path_buf(), vec![config_path1])?;
let existing = Existing {
all: false,
indices: vec![],
};
let result = existing.load(test_dir.to_path_buf());
assert!(result.is_err());
Ok(())
}
#[test]
fn test_existing_load_multiple_sandboxes_no_indices() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let sandbox1 = test_dir.join("sandbox1");
let sandbox2 = test_dir.join("sandbox2");
fs::create_dir_all(&sandbox1)?;
fs::create_dir_all(&sandbox2)?;
let config_path1 = ConfigRootPath::from(sandbox1.clone());
let config_file_path1 = ConfigFilePath::from(config_path1.clone());
fs::create_dir_all(config_file_path1.as_ref().parent().unwrap())?;
fs::write(config_file_path1.as_ref(), "dummy config")?;
let config_path2 = ConfigRootPath::from(sandbox2.clone());
let config_file_path2 = ConfigFilePath::from(config_path2.clone());
fs::create_dir_all(config_file_path2.as_ref().parent().unwrap())?;
fs::write(config_file_path2.as_ref(), "dummy config")?;
crate::save::save(test_dir.to_path_buf(), vec![config_path1, config_path2])?;
let existing = Existing {
all: false,
indices: vec![],
};
let result = existing.load(test_dir.to_path_buf());
assert!(result.is_err());
Ok(())
}
#[test]
fn test_existing_load_no_sandboxes() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let test_dir = temp_dir.path();
let result = Existing {
all: false,
indices: vec![],
}
.load(test_dir.to_path_buf());
assert!(result.is_err());
let result = Existing {
all: true,
indices: vec![],
}
.load(test_dir.to_path_buf());
assert!(result.is_err());
Ok(())
}
}