Skip to content
This repository has been archived by the owner on Feb 3, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1939 from holochain/hc-run-bundles
Browse files Browse the repository at this point in the history
Run hApp-bundles during develpment with `hc run`
  • Loading branch information
thedavidmeister authored Dec 5, 2019
2 parents 270ccde + e2d7984 commit 5c146f6
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- Adds smarter ordering of pending validation. Builds a dependency graph and only will try to validate entries that do not have dependencies also awaiting validation as this will always fail. [#1924](https://github.com/holochain/holochain-rust/pull/1924)
- Adds support for [hApp-bundles](https://github.com/holochain/holoscape/tree/master/example-bundles) to `hc run`. This enables complex hApp setups with multiple DNAs and bridges to be easily run during development without having to write/maintain a conductor config file. [#1939](https://github.com/holochain/holochain-rust/pull/1939)

### Changed

Expand Down
4 changes: 2 additions & 2 deletions crates/cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod hash_dna;
mod init;
mod keygen;
pub mod package;
mod run;
pub mod run;
pub mod test;

pub use self::{
Expand All @@ -14,6 +14,6 @@ pub use self::{
init::init,
keygen::keygen,
package::package,
run::{get_interface_type_string, hc_run_configuration, run},
run::{get_interface_type_string, hc_run_bundle_configuration, hc_run_configuration, run},
test::{test, TEST_DIR_NAME},
};
22 changes: 22 additions & 0 deletions crates/cli/src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use holochain_common::env_vars::EnvVar;
use holochain_conductor_lib::{
conductor::{mount_conductor_from_config, Conductor, CONDUCTOR},
config::*,
happ_bundle::HappBundle,
key_loaders::{test_keystore, test_keystore_loader},
keystore::PRIMARY_KEYBUNDLE_ID,
logger::LogRules,
Expand Down Expand Up @@ -36,6 +37,9 @@ pub fn run(

conductor.start_all_interfaces();
conductor.start_all_instances()?;
conductor
.start_all_static_servers()
.map_err(|e| failure::err_msg(e))?;

println!(
"Holochain development conductor started. Running {} server on port {}",
Expand Down Expand Up @@ -88,6 +92,24 @@ pub fn hc_run_configuration(
})
}

pub fn hc_run_bundle_configuration(
bundle: &HappBundle,
port: u16,
persist: bool,
networked: bool,
logging: bool,
) -> DefaultResult<Configuration> {
bundle
.build_conductor_config(
port,
agent_configuration(),
storage_configuration(persist)?,
networking_configuration(networked),
logger_configuration(logging),
)
.map_err(|e| failure::err_msg(e))
}

// AGENT
const AGENT_NAME_DEFAULT: &str = "testAgent";
const AGENT_CONFIG_ID: &str = "hc-run-agent";
Expand Down
40 changes: 30 additions & 10 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ mod error;
mod util;

use crate::error::{HolochainError, HolochainResult};
use std::{path::PathBuf, str::FromStr};
use holochain_conductor_lib::happ_bundle::HappBundle;
use std::{fs::File, io::Read, path::PathBuf, str::FromStr};
use structopt::StructOpt;

#[derive(StructOpt)]
Expand Down Expand Up @@ -203,15 +204,34 @@ fn run() -> HolochainResult<()> {
let dna_path = dna_path
.unwrap_or(util::std_package_path(&project_path).map_err(HolochainError::Default)?);
let interface_type = cli::get_interface_type_string(interface);
let conductor_config = cli::hc_run_configuration(
&dna_path,
port,
persist,
networked,
&interface_type,
logging,
)
.map_err(HolochainError::Default)?;

let bundle_path = project_path.join("bundle.toml");
let conductor_config = if bundle_path.exists() {
let mut f = File::open(bundle_path)
.map_err(|e| HolochainError::Default(format_err!("{}", e)))?;
let mut contents = String::new();
f.read_to_string(&mut contents)
.map_err(|e| HolochainError::Default(format_err!("{}", e)))?;
let happ_bundle =
toml::from_str::<HappBundle>(&contents).expect("Error loading bundle.");

cli::hc_run_bundle_configuration(&happ_bundle, port, persist, networked, logging)
.map_err(HolochainError::Default)?
} else {
cli::hc_run_configuration(
&dna_path,
port,
persist,
networked,
&interface_type,
logging,
)
.map_err(HolochainError::Default)?
};
println!(
"Booting conductor with following configuration: {:?}",
conductor_config
);
cli::run(dna_path, package, port, interface_type, conductor_config)
.map_err(HolochainError::Default)?
}
Expand Down
25 changes: 1 addition & 24 deletions crates/conductor_lib/src/conductor/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
error::HolochainInstanceError,
key_loaders::test_keystore,
keystore::{Keystore, PRIMARY_KEYBUNDLE_ID},
port_utils::{try_with_port, INTERFACE_CONNECT_ATTEMPTS_MAX},
Holochain,
};
use crossbeam_channel::{unbounded, Receiver, Sender};
Expand Down Expand Up @@ -61,9 +62,6 @@ use holochain_net::{
p2p_network::P2pNetwork,
};

const INTERFACE_CONNECT_ATTEMPTS_MAX: usize = 30;
const INTERFACE_CONNECT_INTERVAL: Duration = Duration::from_secs(1);

lazy_static! {
/// This is a global and mutable Conductor singleton.
/// (Ok, not really. I've made Conductor::from_config public again so holochain_nodejs
Expand Down Expand Up @@ -1416,27 +1414,6 @@ impl Conductor {
}
}

fn try_with_port<T, F: FnOnce() -> T>(port: u16, f: F) -> T {
let mut attempts = 0;
while attempts <= INTERFACE_CONNECT_ATTEMPTS_MAX {
if port_is_available(port) {
return f();
}
warn!(
"Waiting for port {} to be available, sleeping (attempt #{})",
port, attempts
);
thread::sleep(INTERFACE_CONNECT_INTERVAL);
attempts += 1;
}
f()
}

fn port_is_available(port: u16) -> bool {
use std::net::TcpListener;
TcpListener::bind(format!("0.0.0.0:{}", port)).is_ok()
}

/// This can eventually be dependency injected for third party Interface definitions
fn _make_interface(interface_config: &InterfaceConfiguration) -> Box<dyn Interface> {
use crate::interface_impls::{http::HttpInterface, websocket::WebsocketInterface};
Expand Down
189 changes: 189 additions & 0 deletions crates/conductor_lib/src/happ_bundle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use crate::{config::*, port_utils::get_free_port};
use boolinator::Boolinator;
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
pub struct HappBundle {
pub instances: Vec<HappBundleInstance>,
pub bridges: Vec<Bridge>,
#[serde(rename = "UIs")]
pub uis: Vec<HappBundleUi>,
}

#[derive(Serialize, Deserialize)]
pub struct HappBundleInstance {
pub name: String,
pub id: String,
pub dna_hash: String,
pub uri: String,
pub dna_properties: Option<HashMap<String, String>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct HappBundleUi {
pub name: String,
pub id: Option<String>,
pub uri: String,
pub instance_references: Vec<HappBundleInstanceReference>,
}

impl HappBundleUi {
pub fn id(&self) -> String {
self.id.clone().unwrap_or_else(|| String::from(""))
}
}

#[derive(Serialize, Deserialize, Debug)]
pub struct HappBundleInstanceReference {
pub ui_handle: String,
pub instance_id: String,
}

impl HappBundle {
pub fn id_references_are_consistent(&self) -> Result<(), String> {
for bridge in self.bridges.iter() {
for id in vec![bridge.callee_id.clone(), bridge.caller_id.clone()] {
self.instances.iter().find(|i| i.id == id).ok_or_else(|| {
format!(
"No instance with ID {} referenced in bridge {:?}",
id, bridge
)
})?;
}
}

for ui in self.uis.iter() {
for reference in ui.instance_references.iter() {
self.instances
.iter()
.find(|i| i.id == reference.instance_id)
.ok_or_else(|| {
format!(
"No instance with ID {} referenced in UI {:?}",
reference.instance_id, ui
)
})?;
}
}
Ok(())
}

pub fn only_file_uris(&self) -> Result<(), String> {
for instance in self.instances.iter() {
instance.uri.starts_with("file:").ok_or_else(|| {
format!(
"Instance {} uses non-file URI which is not supported in `hc run`",
instance.id
)
})?;
}

for ui in self.uis.iter() {
ui.uri.starts_with("dir:").ok_or_else(|| {
format!(
"UI {} uses non-dir URI which is not supported in `hc run`",
ui.id()
)
})?;
}

Ok(())
}

pub fn build_conductor_config(
&self,
ui_port: u16,
agent_config: AgentConfiguration,
storage: StorageConfiguration,
network: Option<NetworkConfig>,
logger: LoggerConfiguration,
) -> Result<Configuration, String> {
self.id_references_are_consistent()?;
self.only_file_uris()?;

let dnas = self
.instances
.iter()
.map(|happ_instance| {
// splitting off "file://"
let file = happ_instance.uri.clone().split_off(5);
DnaConfiguration {
id: happ_instance.id.clone(),
file,
hash: happ_instance.dna_hash.clone(),
uuid: None,
}
})
.collect::<Vec<_>>();

let instances = self
.instances
.iter()
.map(|happ_instance| InstanceConfiguration {
id: happ_instance.id.clone(),
dna: happ_instance.id.clone(),
agent: agent_config.id.clone(),
storage: storage.clone(),
})
.collect::<Vec<_>>();

let mut interfaces = Vec::new();
let mut ui_bundles = Vec::new();
let mut ui_interfaces = Vec::new();

const MIN_INTERFACE_PORT: u16 = 50000;
const MAX_INTERFACE_PORT: u16 = 60000;
let mut next_interface_port: u16 = MIN_INTERFACE_PORT;
let mut next_ui_port = ui_port;

for ui in self.uis.iter() {
let port = get_free_port(next_interface_port..MAX_INTERFACE_PORT)
.ok_or_else(|| String::from("Couldn't acquire free port"))?;
next_interface_port = port + 1;
interfaces.push(InterfaceConfiguration {
id: ui.id(),
driver: InterfaceDriver::Websocket { port },
admin: false,
instances: ui
.instance_references
.iter()
.map(|ui_ref| InstanceReferenceConfiguration {
id: ui_ref.instance_id.clone(),
alias: Some(ui_ref.ui_handle.clone()),
})
.collect(),
});

ui_bundles.push(UiBundleConfiguration {
id: ui.id(),
root_dir: ui.uri.clone().split_off(4), // splitting off "dir://"
hash: None,
});

let port = get_free_port(next_ui_port..MIN_INTERFACE_PORT - 1)
.ok_or_else(|| String::from("Couldn't acquire free port"))?;
next_ui_port = port + 1;
ui_interfaces.push(UiInterfaceConfiguration {
id: ui.id(),
bundle: ui.id(),
port,
dna_interface: Some(ui.id()),
reroute_to_root: false,
bind_address: String::from("127.0.0.1"),
});
}

Ok(Configuration {
agents: vec![agent_config],
dnas,
instances,
bridges: self.bridges.clone(),
interfaces,
ui_bundles,
ui_interfaces,
network,
logger,
..Default::default()
})
}
}
2 changes: 2 additions & 0 deletions crates/conductor_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ pub mod context_builder;
pub mod dna_location;
pub mod dpki_instance;
pub mod error;
pub mod happ_bundle;
pub mod holo_signing_service;
pub mod holochain;
pub mod interface;
pub mod interface_impls;
pub mod key_loaders;
pub mod keystore;
pub mod logger;
pub mod port_utils;
pub mod signal_wrapper;
pub mod static_file_server;
pub mod static_server_impls;
Expand Down
Loading

0 comments on commit 5c146f6

Please sign in to comment.