use crate::{
conductor::{base::notify, Conductor},
config::{UiBundleConfiguration, UiInterfaceConfiguration},
error::HolochainInstanceError,
static_file_server::ConductorStaticFileServer,
static_server_impls::NickelStaticServer as StaticServer,
};
use holochain_core_types::error::HolochainError;
use std::{path::PathBuf, sync::Arc};
#[allow(clippy::ptr_arg)]
pub trait ConductorUiAdmin {
fn install_ui_bundle_from_file(
&mut self,
path: PathBuf,
id: &String,
copy: bool,
) -> Result<(), HolochainError>;
fn uninstall_ui_bundle(&mut self, id: &String) -> Result<(), HolochainError>;
fn add_ui_interface(
&mut self,
new_instance: UiInterfaceConfiguration,
) -> Result<(), HolochainError>;
fn remove_ui_interface(&mut self, id: &String) -> Result<(), HolochainError>;
fn start_ui_interface(&mut self, id: &String) -> Result<(), HolochainInstanceError>;
fn stop_ui_interface(&mut self, id: &String) -> Result<(), HolochainInstanceError>;
}
#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_CONDUCTOR_LIB)]
#[allow(clippy::ptr_arg, clippy::match_bool)]
impl ConductorUiAdmin for Conductor {
fn install_ui_bundle_from_file(
&mut self,
path: PathBuf,
id: &String,
copy: bool,
) -> Result<(), HolochainError> {
let path = match copy {
true => {
let dest = self.config.persistence_dir.join("static").join(id);
Arc::get_mut(&mut self.ui_dir_copier).unwrap()(&path, &dest).map_err(|e| {
HolochainError::ErrorGeneric(format!(
"Error copying DNA from {} to {}: {}",
path.display(),
dest.display(),
e
))
})?;
dest
}
false => path,
};
let path_string = path
.to_str()
.ok_or_else(|| HolochainError::ConfigError("invalid path".into()))?;
let new_bundle = UiBundleConfiguration {
id: id.to_string(),
root_dir: path_string.into(),
hash: None,
};
let mut new_config = self.config.clone();
new_config.ui_bundles.push(new_bundle);
new_config.check_consistency(&mut self.dna_loader)?;
self.config = new_config;
self.save_config()?;
notify(format!(
"Installed UI bundle from {} as \"{}\"",
path_string, id
));
Ok(())
}
fn uninstall_ui_bundle(&mut self, id: &String) -> Result<(), HolochainError> {
let mut new_config = self.config.clone();
new_config.ui_bundles = new_config
.ui_bundles
.into_iter()
.filter(|bundle| bundle.id != *id)
.collect();
if new_config.ui_bundles.len() == self.config.ui_bundles.len() {
return Err(HolochainError::ConfigError(format!(
"No UI bundles match the given ID \"{}\"",
id
)));
}
let to_remove = new_config
.ui_interfaces
.clone()
.into_iter()
.filter(|ui_interface| ui_interface.bundle == *id);
for bundle_interface in to_remove {
self.remove_ui_interface(&bundle_interface.id)?;
}
new_config.check_consistency(&mut self.dna_loader)?;
self.config = new_config;
self.save_config()?;
Ok(())
}
fn add_ui_interface(
&mut self,
new_interface: UiInterfaceConfiguration,
) -> Result<(), HolochainError> {
let mut new_config = self.config.clone();
new_config.ui_interfaces.push(new_interface.clone());
new_config.check_consistency(&mut self.dna_loader)?;
self.config = new_config;
self.save_config()?;
self.static_servers.insert(
new_interface.id.clone(),
StaticServer::from_configs(
new_interface.clone(),
self.config.ui_bundle_by_id(&new_interface.bundle).unwrap(),
None,
),
);
Ok(())
}
fn remove_ui_interface(&mut self, id: &String) -> Result<(), HolochainError> {
let to_stop = self
.config
.clone()
.ui_interfaces
.into_iter()
.filter(|ui_interface| ui_interface.id == *id);
for ui_interface in to_stop {
let _ = self.stop_ui_interface(&ui_interface.id);
}
let mut new_config = self.config.clone();
new_config.ui_interfaces = new_config
.ui_interfaces
.into_iter()
.filter(|ui_interface| ui_interface.id != *id)
.collect();
new_config.check_consistency(&mut self.dna_loader)?;
self.config = new_config;
self.save_config()?;
self.static_servers
.remove(id)
.ok_or_else(|| HolochainError::ErrorGeneric("Could not remove server".into()))?;
Ok(())
}
fn start_ui_interface(&mut self, id: &String) -> Result<(), HolochainInstanceError> {
let server = self.static_servers.get_mut(id)?;
notify(format!("Starting UI interface \"{}\"...", id));
server.start()
}
fn stop_ui_interface(&mut self, id: &String) -> Result<(), HolochainInstanceError> {
let server = self.static_servers.get_mut(id)?;
notify(format!("Stopping UI interface \"{}\"...", id));
server.stop()
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::conductor::{admin::tests::*, base::UiDirCopier};
use std::{fs::File, io::Read, net::Ipv4Addr};
pub fn test_ui_copier() -> UiDirCopier {
let copier = Box::new(|_source: &PathBuf, _dest: &PathBuf| Ok(()))
as Box<dyn FnMut(&PathBuf, &PathBuf) -> Result<(), HolochainError> + Send + Sync>;
Arc::new(copier)
}
#[test]
fn test_install_ui_bundle_from_file() {
let test_name = "test_install_ui_bundle_from_file";
let mut conductor = create_test_conductor(test_name, 3000);
let bundle_path = PathBuf::from(".");
assert_eq!(
conductor.install_ui_bundle_from_file(
bundle_path,
&"test-bundle-id".to_string(),
false
),
Ok(())
);
let mut config_contents = String::new();
let mut file =
File::open(&conductor.config_path()).expect("Could not open temp config file");
file.read_to_string(&mut config_contents)
.expect("Could not read temp config file");
let mut toml = empty_bridges();
toml = add_line(toml, persistence_dir(test_name));
toml = add_line(toml, empty_ui_interfaces());
toml = add_block(toml, agent1());
toml = add_block(toml, agent2());
toml = add_block(toml, dna());
toml = add_block(toml, instance1());
toml = add_block(toml, instance2());
toml = add_block(toml, interface(3000));
toml = add_block(
toml,
String::from(
r#"[[ui_bundles]]
id = 'test-bundle-id'
root_dir = '.'"#,
),
);
toml = add_block(toml, logger());
toml = add_block(toml, passphrase_service());
toml = add_block(toml, signals());
toml = format!("{}\n", toml);
assert_eq!(config_contents, toml,)
}
#[test]
fn test_install_ui_bundle_from_file_and_copy() {
let test_name = "test_install_ui_bundle_from_file_and_copy";
let mut conductor = create_test_conductor(test_name, 3100);
conductor.ui_dir_copier = test_ui_copier();
let bundle_path = PathBuf::from(".");
assert_eq!(
conductor.install_ui_bundle_from_file(bundle_path, &"test-bundle-id".to_string(), true),
Ok(())
);
let mut config_contents = String::new();
let mut file =
File::open(&conductor.config_path()).expect("Could not open temp config file");
file.read_to_string(&mut config_contents)
.expect("Could not read temp config file");
let dest = conductor
.config
.persistence_dir
.join("static")
.join("test-bundle-id");
let mut toml = empty_bridges();
toml = add_line(toml, persistence_dir(test_name));
toml = add_line(toml, empty_ui_interfaces());
toml = add_block(toml, agent1());
toml = add_block(toml, agent2());
toml = add_block(toml, dna());
toml = add_block(toml, instance1());
toml = add_block(toml, instance2());
toml = add_block(toml, interface(3100));
toml = add_block(
toml,
String::from(
r#"[[ui_bundles]]
id = 'test-bundle-id'"#,
),
);
toml = add_line(toml, format!("root_dir = '{}'", dest.display()));
toml = add_block(toml, logger());
toml = add_block(toml, passphrase_service());
toml = add_block(toml, signals());
toml = format!("{}\n", toml);
assert_eq!(config_contents, toml,)
}
#[test]
fn test_uninstall_ui_bundle() {
let test_name = "test_uninstall_ui_bundle";
let mut conductor = create_test_conductor(test_name, 3001);
assert_eq!(
conductor.uninstall_ui_bundle(&"test-bundle-id".to_string()),
Err(HolochainError::ConfigError(
"No UI bundles match the given ID \"test-bundle-id\"".into()
))
);
let bundle_path = PathBuf::from(".");
assert_eq!(
conductor.install_ui_bundle_from_file(
bundle_path,
&"test-bundle-id".to_string(),
false
),
Ok(())
);
assert_eq!(
conductor.uninstall_ui_bundle(&"test-bundle-id".to_string()),
Ok(())
);
let mut config_contents = String::new();
let mut file =
File::open(&conductor.config_path()).expect("Could not open temp config file");
file.read_to_string(&mut config_contents)
.expect("Could not read temp config file");
let mut toml = header_block(test_name);
toml = add_block(toml, agent1());
toml = add_block(toml, agent2());
toml = add_block(toml, dna());
toml = add_block(toml, instance1());
toml = add_block(toml, instance2());
toml = add_block(toml, interface(3001));
toml = add_block(toml, logger());
toml = add_block(toml, passphrase_service());
toml = add_block(toml, signals());
toml = format!("{}\n", toml);
assert_eq!(config_contents, toml,)
}
#[test]
fn test_add_ui_interface() {
let test_name = "test_add_ui_interface";
let mut conductor = create_test_conductor(test_name, 3002);
assert_eq!(
conductor.add_ui_interface(UiInterfaceConfiguration {
id: "test-ui-interface-id".into(),
port: 4000,
bundle: "test-bundle-id".into(),
dna_interface: None,
reroute_to_root: true,
bind_address: Ipv4Addr::LOCALHOST.to_string()
}),
Err(HolochainError::ErrorGeneric(
"UI bundle configuration test-bundle-id not found, mentioned in UI interface test-ui-interface-id".into()
))
);
let bundle_path = PathBuf::from(".");
assert_eq!(
conductor.install_ui_bundle_from_file(
bundle_path,
&"test-bundle-id".to_string(),
false
),
Ok(())
);
assert_eq!(
conductor.add_ui_interface(UiInterfaceConfiguration {
id: "test-ui-interface-id".into(),
port: 4000,
bundle: "test-bundle-id".into(),
dna_interface: None,
reroute_to_root: true,
bind_address: Ipv4Addr::LOCALHOST.to_string()
}),
Ok(())
);
let mut config_contents = String::new();
let mut file =
File::open(&conductor.config_path()).expect("Could not open temp config file");
file.read_to_string(&mut config_contents)
.expect("Could not read temp config file");
let mut toml = empty_bridges();
toml = add_line(toml, persistence_dir(test_name));
toml = add_block(toml, agent1());
toml = add_block(toml, agent2());
toml = add_block(toml, dna());
toml = add_block(toml, instance1());
toml = add_block(toml, instance2());
toml = add_block(toml, interface(3002));
toml = add_block(
toml,
String::from(
r#"[[ui_bundles]]
id = 'test-bundle-id'
root_dir = '.'
[[ui_interfaces]]
bind_address = '127.0.0.1'
bundle = 'test-bundle-id'
id = 'test-ui-interface-id'
port = 4000
reroute_to_root = true"#,
),
);
toml = add_block(toml, logger());
toml = add_block(toml, passphrase_service());
toml = add_block(toml, signals());
toml = format!("{}\n", toml);
assert_eq!(config_contents, toml);
}
#[test]
fn test_remove_ui_interface() {
let test_name = "test_remove_ui_interface";
let mut conductor = create_test_conductor(test_name, 3003);
assert_eq!(
conductor.remove_ui_interface(&"test-ui-interface-id".to_string()),
Err(HolochainError::ErrorGeneric(
"Could not remove server".into()
))
);
let bundle_path = PathBuf::from(".");
assert_eq!(
conductor.install_ui_bundle_from_file(
bundle_path,
&"test-bundle-id".to_string(),
false
),
Ok(())
);
assert_eq!(
conductor.add_ui_interface(UiInterfaceConfiguration {
id: "test-ui-interface-id".into(),
port: 4000,
bundle: "test-bundle-id".into(),
dna_interface: None,
reroute_to_root: true,
bind_address: Ipv4Addr::LOCALHOST.to_string()
}),
Ok(())
);
assert_eq!(
conductor.remove_ui_interface(&"test-ui-interface-id".to_string()),
Ok(())
);
let mut config_contents = String::new();
let mut file =
File::open(&conductor.config_path()).expect("Could not open temp config file");
file.read_to_string(&mut config_contents)
.expect("Could not read temp config file");
let mut toml = empty_bridges();
toml = add_line(toml, persistence_dir(test_name));
toml = add_line(toml, empty_ui_interfaces());
toml = add_block(toml, agent1());
toml = add_block(toml, agent2());
toml = add_block(toml, dna());
toml = add_block(toml, instance1());
toml = add_block(toml, instance2());
toml = add_block(toml, interface(3003));
toml = add_block(
toml,
String::from(
r#"[[ui_bundles]]
id = 'test-bundle-id'
root_dir = '.'"#,
),
);
toml = add_block(toml, logger());
toml = add_block(toml, passphrase_service());
toml = add_block(toml, signals());
toml = format!("{}\n", toml);
assert_eq!(config_contents, toml);
}
#[test]
fn test_start_ui_interface() {
let test_name = "test_start_ui_interface";
let mut conductor =
create_test_conductor_from_toml(&barebones_test_toml(test_name), test_name);
let bundle_path = PathBuf::from(".");
assert_eq!(
conductor.install_ui_bundle_from_file(
bundle_path,
&"test-bundle-id".to_string(),
false
),
Ok(())
);
assert_eq!(
conductor.add_ui_interface(UiInterfaceConfiguration {
id: "test-ui-interface-id".into(),
port: 4100,
bundle: "test-bundle-id".into(),
dna_interface: None,
reroute_to_root: true,
bind_address: Ipv4Addr::LOCALHOST.to_string()
}),
Ok(())
);
assert_eq!(
conductor.start_ui_interface(&"test-ui-interface-id".to_string()),
Ok(())
);
}
#[test]
fn test_stop_ui_interface() {
let test_name = "test_stop_ui_interface";
let mut conductor =
create_test_conductor_from_toml(&barebones_test_toml(test_name), test_name);
let bundle_path = PathBuf::from(".");
assert_eq!(
conductor.install_ui_bundle_from_file(
bundle_path,
&"test-bundle-id".to_string(),
false
),
Ok(())
);
assert_eq!(
conductor.add_ui_interface(UiInterfaceConfiguration {
id: "test-ui-interface-id".into(),
port: 4101,
bundle: "test-bundle-id".into(),
dna_interface: None,
reroute_to_root: true,
bind_address: Ipv4Addr::LOCALHOST.to_string()
}),
Ok(())
);
assert_eq!(
conductor.stop_ui_interface(&"test-ui-interface-id".to_string()),
Err(HolochainInstanceError::InternalFailure(
HolochainError::ErrorGeneric("server is already stopped".into())
))
);
assert_eq!(
conductor.start_ui_interface(&"test-ui-interface-id".to_string()),
Ok(())
);
assert_eq!(
conductor.stop_ui_interface(&"test-ui-interface-id".to_string()),
Ok(())
);
}
}