diff --git a/Cargo.toml b/Cargo.toml index ba042ccc0..e5cfc5847 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "iceoryx2-cli/iox2-rpc", "iceoryx2-cli/iox2-services", "iceoryx2-cli/iox2-sub", + "iceoryx2-cli/utils", "examples", @@ -65,6 +66,8 @@ iceoryx2-pal-concurrency-sync = { version = "0.3.0", path = "iceoryx2-pal/concur iceoryx2-pal-posix = { version = "0.3.0", path = "iceoryx2-pal/posix/" } iceoryx2-pal-configuration = { version = "0.3.0", path = "iceoryx2-pal/configuration/" } +iceoryx2-cli-utils = { version = "0.3.0", path = "iceoryx2-cli/utils/" } + iceoryx2-cal = { version = "0.3.0", path = "iceoryx2-cal" } iceoryx2-ffi = { version = "0.3.0", path = "iceoryx2-ffi/ffi" } @@ -91,7 +94,10 @@ once_cell = { version = "1.19.0" } ouroboros = { version = "0.18.4" } proc-macro2 = { version = "1.0.84" } quote = { version = "1.0.36" } +ron = { version = "0.8" } serde = { version = "1.0.203", features = ["derive"] } +serde_yaml = { version = "0.9.34" } +serde_json = { version = "1.0" } serde_test = { version = "1.0.176" } sha1_smol = { version = "1.0.0" } syn = { version = "2.0.66", features = ["full"] } diff --git a/iceoryx2-cli/iox2-services/Cargo.toml b/iceoryx2-cli/iox2-services/Cargo.toml index f787a5a57..9621a09c4 100644 --- a/iceoryx2-cli/iox2-services/Cargo.toml +++ b/iceoryx2-cli/iox2-services/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iox2-services" -description = "Iceoryx2: CLI for managing iceoryx2 services" +description = "CLI for managing iceoryx2 services" categories = { workspace = true } edition = { workspace = true } homepage = { workspace = true } @@ -13,3 +13,13 @@ version = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +iceoryx2 = { workspace = true } +iceoryx2-bb-log = { workspace = true } +iceoryx2-cli-utils = { workspace = true } + +anyhow = { workspace = true } +better-panic = { workspace = true } +clap = { workspace = true } +human-panic = { workspace = true } +serde = { workspace = true } + diff --git a/iceoryx2-cli/iox2-services/src/cli.rs b/iceoryx2-cli/iox2-services/src/cli.rs new file mode 100644 index 000000000..881b1ac83 --- /dev/null +++ b/iceoryx2-cli/iox2-services/src/cli.rs @@ -0,0 +1,71 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use clap::Args; +use clap::Parser; +use clap::Subcommand; +use clap::ValueEnum; + +use iceoryx2_cli_utils::help_template; + +use crate::Format; + +#[derive(Parser)] +#[command( + name = "iox2-services", + about = "Query information about iceoryx2 services", + long_about = None, + version = env!("CARGO_PKG_VERSION"), + disable_help_subcommand = true, + arg_required_else_help = false, + help_template = help_template("iox2-services", false), +)] +pub struct Cli { + #[clap(subcommand)] + pub action: Option, + + #[clap(long, short = 'f', value_enum, global = true)] + pub format: Option, +} + +#[derive(Debug, Clone, ValueEnum)] +#[clap(rename_all = "PascalCase")] +#[derive(Default)] +pub enum MessagingPatternFilter { + PublishSubscribe, + Event, + #[default] + All, +} + +#[derive(Debug, Clone, Args)] +pub struct DetailsFilter { + #[clap(short, long, value_enum, default_value_t = MessagingPatternFilter::All)] + pub pattern: MessagingPatternFilter, +} + +#[derive(Parser)] +pub struct DetailsOptions { + #[clap(help = "Name of the service e.g. \"My Service\"")] + pub service: String, + + #[command(flatten)] + pub filter: DetailsFilter, +} + +#[derive(Subcommand)] +pub enum Action { + #[clap(about = "List all existing services")] + List, + #[clap(about = "Show details of an existing service")] + Details(DetailsOptions), +} diff --git a/iceoryx2-cli/iox2-services/src/commands.rs b/iceoryx2-cli/iox2-services/src/commands.rs new file mode 100644 index 000000000..64299207a --- /dev/null +++ b/iceoryx2-cli/iox2-services/src/commands.rs @@ -0,0 +1,72 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::cli::DetailsFilter; +use crate::output::*; +use anyhow::{Context, Error, Result}; +use iceoryx2::prelude::*; +use iceoryx2_cli_utils::Filter; +use iceoryx2_cli_utils::Format; + +pub fn list(format: Format) -> Result<()> { + let mut services = ServiceList::new(); + + ipc::Service::list(Config::global_config(), |service| { + services.push(ServiceDescriptor::from(service)); + CallbackProgression::Continue + }) + .context("failed to retrieve services")?; + + services.sort_by_key(|pattern| match pattern { + ServiceDescriptor::PublishSubscribe(name) => (name.clone(), 0), + ServiceDescriptor::Event(name) => (name.clone(), 1), + ServiceDescriptor::Undefined(name) => (name.to_string(), 2), + }); + + print!("{}", format.as_string(&services)?); + + Ok(()) +} + +pub fn details(service_name: String, filter: DetailsFilter, format: Format) -> Result<()> { + let mut error: Option = None; + + ipc::Service::list(Config::global_config(), |service| { + if service_name == service.static_details.name().to_string() { + let description = ServiceDescription::from(&service); + + if filter.matches(&description) { + match format.as_string(&description) { + Ok(output) => { + print!("{}", output); + CallbackProgression::Continue + } + Err(e) => { + error = Some(e); + CallbackProgression::Stop + } + } + } else { + // Filter did not match + CallbackProgression::Continue + } + } else { + // Service name did not match + CallbackProgression::Continue + } + })?; + + if let Some(err) = error { + return Err(err); + } + Ok(()) +} diff --git a/iceoryx2-cli/iox2-services/src/filter.rs b/iceoryx2-cli/iox2-services/src/filter.rs new file mode 100644 index 000000000..3ec0cd060 --- /dev/null +++ b/iceoryx2-cli/iox2-services/src/filter.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::cli::DetailsFilter; +use crate::cli::MessagingPatternFilter; +use crate::output::ServiceDescription; +use iceoryx2::service::static_config::messaging_pattern::MessagingPattern; +use iceoryx2_cli_utils::Filter; + +impl Filter for MessagingPatternFilter { + fn matches(&self, description: &ServiceDescription) -> bool { + matches!( + (self, &description.pattern), + ( + MessagingPatternFilter::PublishSubscribe, + MessagingPattern::PublishSubscribe(_) + ) | (MessagingPatternFilter::Event, MessagingPattern::Event(_)) + | (MessagingPatternFilter::All, _) + ) + } +} + +impl Filter for DetailsFilter { + fn matches(&self, description: &ServiceDescription) -> bool { + self.pattern.matches(description) + } +} diff --git a/iceoryx2-cli/iox2-services/src/main.rs b/iceoryx2-cli/iox2-services/src/main.rs index 6cacb56d6..a2d393f47 100644 --- a/iceoryx2-cli/iox2-services/src/main.rs +++ b/iceoryx2-cli/iox2-services/src/main.rs @@ -10,6 +10,64 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +#[cfg(not(debug_assertions))] +use human_panic::setup_panic; +#[cfg(debug_assertions)] +extern crate better_panic; + +mod cli; +mod commands; +mod filter; +mod output; + +use clap::CommandFactory; +use clap::Parser; +use cli::Action; +use cli::Cli; +use iceoryx2_bb_log::{set_log_level, LogLevel}; +use iceoryx2_cli_utils::Format; + fn main() { - println!("Not implemented. Stay tuned!"); + #[cfg(not(debug_assertions))] + { + setup_panic!(); + } + #[cfg(debug_assertions)] + { + better_panic::Settings::debug() + .most_recent_first(false) + .lineno_suffix(true) + .verbosity(better_panic::Verbosity::Full) + .install(); + } + + set_log_level(LogLevel::Warn); + + match Cli::try_parse() { + Ok(cli) => { + if let Some(action) = cli.action { + match action { + Action::List => { + if let Err(e) = commands::list(cli.format.unwrap_or(Format::Ron)) { + eprintln!("Failed to list services: {}", e); + } + } + Action::Details(options) => { + if let Err(e) = commands::details( + options.service, + options.filter, + cli.format.unwrap_or(Format::Ron), + ) { + eprintln!("Failed to retrieve service details: {}", e); + } + } + } + } else { + Cli::command().print_help().expect("Failed to print help"); + } + } + Err(e) => { + eprintln!("{}", e); + } + } } diff --git a/iceoryx2-cli/iox2-services/src/output.rs b/iceoryx2-cli/iox2-services/src/output.rs new file mode 100644 index 000000000..528658fb9 --- /dev/null +++ b/iceoryx2-cli/iox2-services/src/output.rs @@ -0,0 +1,152 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use iceoryx2::node::NodeId; +use iceoryx2::node::NodeState; +use iceoryx2::node::NodeView; +use iceoryx2::service::attribute::AttributeSet; +use iceoryx2::service::static_config::messaging_pattern::MessagingPattern; +use iceoryx2::service::Service; +use iceoryx2::service::ServiceDetails; +use iceoryx2::service::ServiceDynamicDetails; + +#[derive(serde::Serialize, Eq, PartialEq, Ord, PartialOrd)] +pub enum ServiceDescriptor { + PublishSubscribe(String), + Event(String), + Undefined(String), +} + +impl From> for ServiceDescriptor +where + T: iceoryx2::service::Service, +{ + fn from(service: ServiceDetails) -> Self { + match service.static_details.messaging_pattern() { + MessagingPattern::PublishSubscribe(_) => { + ServiceDescriptor::PublishSubscribe(service.static_details.name().to_string()) + } + MessagingPattern::Event(_) => { + ServiceDescriptor::Event(service.static_details.name().to_string()) + } + _ => ServiceDescriptor::Undefined("Undefined".to_string()), + } + } +} + +pub type ServiceList = Vec; + +#[derive(serde::Serialize)] +pub enum ServiceNodeState { + Alive, + Dead, + Inaccessible, + Undefined, +} + +#[derive(serde::Serialize)] +pub struct ServiceNodeDetails { + state: ServiceNodeState, + id: NodeId, + name: Option, + executable: Option, +} + +impl From<&NodeState> for ServiceNodeDetails +where + T: Service, +{ + fn from(node_state: &NodeState) -> Self { + match node_state { + NodeState::Alive(view) => ServiceNodeDetails { + state: ServiceNodeState::Alive, + id: *view.id(), + name: view + .details() + .as_ref() + .map(|details| details.name().as_str().to_string()), + executable: view + .details() + .as_ref() + .map(|details| details.executable().to_string()), + }, + NodeState::Dead(view) => ServiceNodeDetails { + state: ServiceNodeState::Dead, + id: *view.id(), + name: view + .details() + .as_ref() + .map(|details| details.name().as_str().to_string()), + executable: view + .details() + .as_ref() + .map(|details| details.executable().to_string()), + }, + NodeState::Inaccessible(node_id) => ServiceNodeDetails { + state: ServiceNodeState::Inaccessible, + id: *node_id, + name: None, + executable: None, + }, + NodeState::Undefined(node_id) => ServiceNodeDetails { + state: ServiceNodeState::Undefined, + id: *node_id, + name: None, + executable: None, + }, + } + } +} + +#[derive(serde::Serialize)] +pub struct ServiceNodeList { + num: usize, + details: Vec, +} + +impl From<&ServiceDynamicDetails> for ServiceNodeList +where + T: Service, +{ + fn from(details: &ServiceDynamicDetails) -> Self { + ServiceNodeList { + num: details.nodes.len(), + details: details.nodes.iter().map(ServiceNodeDetails::from).collect(), + } + } +} + +#[derive(serde::Serialize)] +pub struct ServiceDescription { + pub service_id: String, + pub service_name: String, + pub attributes: AttributeSet, + pub pattern: MessagingPattern, + pub nodes: Option, +} + +impl From<&ServiceDetails> for ServiceDescription +where + T: Service, +{ + fn from(service: &ServiceDetails) -> Self { + let config = &service.static_details; + + ServiceDescription { + service_id: config.service_id().as_str().to_string(), + service_name: config.name().as_str().to_string(), + attributes: config.attributes().clone(), + pattern: config.messaging_pattern().clone(), + nodes: service.dynamic_details.as_ref().map(ServiceNodeList::from), + } + } +} diff --git a/iceoryx2-cli/iox2/Cargo.toml b/iceoryx2-cli/iox2/Cargo.toml index 2865f5473..bd71143ca 100644 --- a/iceoryx2-cli/iox2/Cargo.toml +++ b/iceoryx2-cli/iox2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iox2" -description = "Iceoryx2: CLI entry-point" +description = "CLI entry-point" categories = { workspace = true } edition = { workspace = true } homepage = { workspace = true } @@ -11,6 +11,8 @@ rust-version = { workspace = true } version = { workspace = true } [dependencies] +iceoryx2-cli-utils = { workspace = true } + anyhow = { workspace = true } better-panic = { workspace = true } colored = { workspace = true } diff --git a/iceoryx2-cli/iox2/src/cli.rs b/iceoryx2-cli/iox2/src/cli.rs index a900c0c6f..37210236d 100644 --- a/iceoryx2-cli/iox2/src/cli.rs +++ b/iceoryx2-cli/iox2/src/cli.rs @@ -11,26 +11,27 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use clap::Parser; -use colored::*; + +use iceoryx2_cli_utils::help_template; #[derive(Parser, Debug)] #[command( name = "iox2", - about = "The command-line interface to iceoryx2", + about = "The command-line interface entrypoint to iceoryx2.", long_about = None, version = env!("CARGO_PKG_VERSION"), disable_help_subcommand = true, - arg_required_else_help = true, - help_template = help_template(), + arg_required_else_help = false, + help_template = help_template("iox2", true), )] pub struct Cli { - #[arg(short, long, help = "List all installed commands")] + #[arg(short, long, help = "List all installed external commands")] pub list: bool, #[arg( short, long, - help = "Display paths that will be checked for installed commands" + help = "Display paths that will be checked for external commands" )] pub paths: bool, @@ -49,16 +50,3 @@ pub struct Cli { )] pub external_command: Vec, } - -fn help_template() -> String { - format!( - "{}{}{}\n\n{}\n{{options}}\n\n{}\n{{subcommands}}{}{}", - "Usage: ".bright_green().bold(), - "iox2 ".bold(), - "[OPTIONS] [COMMAND]", - "Options:".bright_green().bold(), - "Commands:".bright_green().bold(), - " ... ".bold(), - "See all installed commands with --list" - ) -} diff --git a/iceoryx2-cli/utils/Cargo.toml b/iceoryx2-cli/utils/Cargo.toml new file mode 100644 index 000000000..41c9206e2 --- /dev/null +++ b/iceoryx2-cli/utils/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "iceoryx2-cli-utils" +description = "Common helpers for iceoryx2 CLI tools." +categories = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +keywords = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +colored = { workspace = true } +toml = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } +serde_json = { workspace = true } +ron = { workspace = true } diff --git a/iceoryx2-cli/utils/src/cli.rs b/iceoryx2-cli/utils/src/cli.rs new file mode 100644 index 000000000..5c23ac2b4 --- /dev/null +++ b/iceoryx2-cli/utils/src/cli.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use colored::*; + +pub fn help_template(cli_name: &str, show_external_commands: bool) -> String { + let mut template = format!( + "{{about}}\n\n{}{}{}[OPTIONS] [COMMAND]\n\n{}\n{{options}}\n\n{}\n{{subcommands}}", + "Usage: ".bright_green().bold(), + cli_name.bold(), + " ".bold(), + "Options:".bright_green().bold(), + "Commands:".bright_green().bold() + ); + + if show_external_commands { + template.push_str(&format!( + "\n{}{}", + " ... ".bold(), + "See external installed commands with --list" + )); + } + + template +} diff --git a/iceoryx2-cli/utils/src/filter.rs b/iceoryx2-cli/utils/src/filter.rs new file mode 100644 index 000000000..e5bab4b91 --- /dev/null +++ b/iceoryx2-cli/utils/src/filter.rs @@ -0,0 +1,17 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::fmt::Debug; + +pub trait Filter: Debug { + fn matches(&self, item: &T) -> bool; +} diff --git a/iceoryx2-cli/utils/src/format.rs b/iceoryx2-cli/utils/src/format.rs new file mode 100644 index 000000000..11c89a1cf --- /dev/null +++ b/iceoryx2-cli/utils/src/format.rs @@ -0,0 +1,53 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use anyhow::{anyhow, Context, Error, Result}; +use clap::ValueEnum; +use serde::Serialize; +use std::str::FromStr; + +#[derive(Clone, Copy, ValueEnum)] +#[value(rename_all = "UPPERCASE")] +pub enum Format { + Ron, + Json, + Yaml, +} + +impl Format { + pub fn as_string(self, data: &T) -> Result { + match self { + Format::Ron => ron::ser::to_string_pretty( + data, + ron::ser::PrettyConfig::new().separate_tuple_members(true), + ) + .context("failed to serialize to RON format"), + Format::Json => { + serde_json::to_string_pretty(data).context("failed to serialize to JSON format") + } + Format::Yaml => serde_yaml::to_string(data).context("failed to serialize to YAML"), + } + } +} + +impl FromStr for Format { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_uppercase().as_str() { + "RON" => Ok(Format::Ron), + "JSON" => Ok(Format::Json), + "YAML" => Ok(Format::Yaml), + _ => Err(anyhow!("unsupported output format '{}'", s)), + } + } +} diff --git a/iceoryx2-cli/utils/src/lib.rs b/iceoryx2-cli/utils/src/lib.rs new file mode 100644 index 000000000..08d3be7d9 --- /dev/null +++ b/iceoryx2-cli/utils/src/lib.rs @@ -0,0 +1,19 @@ +// Copyright (c) 2024 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod cli; +mod filter; +mod format; + +pub use cli::help_template; +pub use filter::Filter; +pub use format::Format; diff --git a/iceoryx2/src/prelude.rs b/iceoryx2/src/prelude.rs index 0c525b02c..b7f5b3bee 100644 --- a/iceoryx2/src/prelude.rs +++ b/iceoryx2/src/prelude.rs @@ -17,7 +17,7 @@ pub use crate::service::messaging_pattern::MessagingPattern; pub use crate::service::{ attribute::AttributeSet, attribute::AttributeSpecifier, attribute::AttributeVerifier, ipc, local, port_factory::publisher::UnableToDeliverStrategy, port_factory::PortFactory, - service_name::ServiceName, Service, + service_name::ServiceName, Service, ServiceDetails, }; pub use iceoryx2_bb_derive_macros::PlacementDefault; pub use iceoryx2_bb_elementary::alignment::Alignment;