diff --git a/iceoryx2-cli/iox2-nodes/Cargo.toml b/iceoryx2-cli/iox2-nodes/Cargo.toml index 5c732e92..7c360fc5 100644 --- a/iceoryx2-cli/iox2-nodes/Cargo.toml +++ b/iceoryx2-cli/iox2-nodes/Cargo.toml @@ -14,6 +14,7 @@ version = { workspace = true } iceoryx2 = { workspace = true } iceoryx2-bb-log = { workspace = true } iceoryx2-cli-utils = { workspace = true } +iceoryx2-pal-posix = {workspace = true} anyhow = { workspace = true } better-panic = { workspace = true } diff --git a/iceoryx2-cli/iox2-nodes/src/cli.rs b/iceoryx2-cli/iox2-nodes/src/cli.rs index 77b34853..46d705c4 100644 --- a/iceoryx2-cli/iox2-nodes/src/cli.rs +++ b/iceoryx2-cli/iox2-nodes/src/cli.rs @@ -10,11 +10,16 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +use std::str::FromStr; + +use clap::Args; use clap::Parser; use clap::Subcommand; +use clap::ValueEnum; use iceoryx2_cli_utils::help_template; use iceoryx2_cli_utils::Format; +use iceoryx2_pal_posix::posix::pid_t; #[derive(Parser)] #[command( @@ -30,20 +35,78 @@ pub struct Cli { #[clap(subcommand)] pub action: Option, - #[clap(long, short = 'f', value_enum, global = true)] - pub format: Option, + #[clap(long, short = 'f', value_enum, global = true, value_enum, default_value_t = Format::Ron)] + pub format: Format, } -#[derive(Parser)] +#[derive(Clone, Debug)] +pub enum NodeIdentifier { + Name(String), + Id(String), + Pid(pid_t), +} + +fn is_valid_hex(s: &str) -> bool { + s.len() == 32 && s.chars().all(|c| c.is_ascii_hexdigit()) +} + +impl FromStr for NodeIdentifier { + type Err = String; + + fn from_str(s: &str) -> Result { + if let Ok(pid) = s.parse::() { + Ok(NodeIdentifier::Pid(pid)) + } else if is_valid_hex(s) { + Ok(NodeIdentifier::Id(s.to_string())) + } else { + Ok(NodeIdentifier::Name(s.to_string())) + } + } +} + +#[derive(Debug, Clone, ValueEnum)] +#[clap(rename_all = "PascalCase")] +#[derive(Default)] +pub enum StateFilter { + Alive, + Dead, + Inaccessible, + Undefined, + #[default] + All, +} + +#[derive(Debug, Clone, Args)] +pub struct ListFilter { + #[clap(short, long, value_enum, default_value_t = StateFilter::All)] + pub state: StateFilter, +} + +#[derive(Args)] +pub struct ListOptions { + #[command(flatten)] + pub filter: ListFilter, +} + +#[derive(Debug, Clone, Args)] +pub struct DetailsFilter { + #[clap(short, long, value_enum, default_value_t = StateFilter::All)] + pub state: StateFilter, +} + +#[derive(Args)] pub struct DetailsOptions { - #[clap(help = "")] - pub node: String, + #[clap(help = "Name, ID or PID")] + pub node: NodeIdentifier, + + #[command(flatten)] + pub filter: DetailsFilter, } #[derive(Subcommand)] pub enum Action { - #[clap(about = "")] - List, - #[clap(about = "")] + #[clap(about = "List all existing nodes")] + List(ListOptions), + #[clap(about = "Show details of an existing node")] Details(DetailsOptions), } diff --git a/iceoryx2-cli/iox2-nodes/src/commands.rs b/iceoryx2-cli/iox2-nodes/src/commands.rs index 6e33d68a..7e5357e4 100644 --- a/iceoryx2-cli/iox2-nodes/src/commands.rs +++ b/iceoryx2-cli/iox2-nodes/src/commands.rs @@ -10,16 +10,24 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use anyhow::{Context, Result}; +use anyhow::{Context, Error, Result}; use iceoryx2::prelude::*; +use iceoryx2_cli_utils::output::NodeDescription; use iceoryx2_cli_utils::output::NodeDescriptor; use iceoryx2_cli_utils::output::NodeList; +use iceoryx2_cli_utils::Filter; use iceoryx2_cli_utils::Format; -pub fn list(format: Format) -> Result<()> { +use crate::cli::DetailsFilter; +use crate::cli::ListFilter; +use crate::cli::NodeIdentifier; + +pub fn list(filter: ListFilter, format: Format) -> Result<()> { let mut nodes = Vec::::new(); - Node::::list(Config::global_config(), |node_state| { - nodes.push(NodeDescriptor::from(&node_state)); + Node::::list(Config::global_config(), |node| { + if filter.matches(&node) { + nodes.push(NodeDescriptor::from(&node)); + } CallbackProgression::Continue }) .context("failed to retrieve nodes")?; @@ -34,3 +42,24 @@ pub fn list(format: Format) -> Result<()> { Ok(()) } + +pub fn details(identifier: NodeIdentifier, filter: DetailsFilter, format: Format) -> Result<()> { + let mut error: Option = None; + + Node::::list(Config::global_config(), |node| { + if identifier.matches(&node) && filter.matches(&node) { + match format.as_string(&NodeDescription::from(&node)) { + Ok(output) => { + print!("{}", output); + } + Err(e) => { + error = Some(e); + } + } + } + CallbackProgression::Continue + }) + .context("failed to retrieve nodes")?; + + Ok(()) +} diff --git a/iceoryx2-cli/iox2-nodes/src/filter.rs b/iceoryx2-cli/iox2-nodes/src/filter.rs new file mode 100644 index 00000000..d5264759 --- /dev/null +++ b/iceoryx2-cli/iox2-nodes/src/filter.rs @@ -0,0 +1,78 @@ +// 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::ListFilter; +use crate::cli::NodeIdentifier; +use crate::cli::StateFilter; +use iceoryx2::node::NodeState; +use iceoryx2::node::NodeView; +use iceoryx2::service::ipc::Service; +use iceoryx2_cli_utils::output::NodeIdString; +use iceoryx2_cli_utils::Filter; + +impl Filter> for NodeIdentifier { + fn matches(&self, node: &NodeState) -> bool { + match self { + NodeIdentifier::Name(ref name) => match node { + NodeState::Alive(view) => view + .details() + .as_ref() + .map(|details| details.name().as_str() == name) + .unwrap_or(false), + NodeState::Dead(view) => view + .details() + .as_ref() + .map(|details| details.name().as_str() == name) + .unwrap_or(false), + NodeState::Inaccessible(_) | NodeState::Undefined(_) => false, + }, + NodeIdentifier::Id(ref id) => match node { + NodeState::Alive(view) => NodeIdString::from(view.id()) == **id, + NodeState::Dead(view) => NodeIdString::from(view.id()) == **id, + NodeState::Inaccessible(node_id) => NodeIdString::from(node_id) == **id, + NodeState::Undefined(node_id) => NodeIdString::from(node_id) == **id, + }, + NodeIdentifier::Pid(pid) => match node { + NodeState::Alive(view) => view.id().pid().value() == *pid, + NodeState::Dead(view) => view.id().pid().value() == *pid, + NodeState::Inaccessible(node_id) => node_id.pid().value() == *pid, + NodeState::Undefined(node_id) => node_id.pid().value() == *pid, + }, + } + } +} + +impl Filter> for StateFilter { + fn matches(&self, node: &NodeState) -> bool { + matches!( + (self, node), + (StateFilter::Alive, NodeState::Alive(_)) + | (StateFilter::Dead, NodeState::Dead(_)) + | (StateFilter::Inaccessible, NodeState::Inaccessible(_)) + | (StateFilter::Undefined, NodeState::Undefined(_)) + | (StateFilter::All, _) + ) + } +} + +impl Filter> for ListFilter { + fn matches(&self, node: &NodeState) -> bool { + self.state.matches(node) + } +} + +impl Filter> for DetailsFilter { + fn matches(&self, node: &NodeState) -> bool { + self.state.matches(node) + } +} diff --git a/iceoryx2-cli/iox2-nodes/src/main.rs b/iceoryx2-cli/iox2-nodes/src/main.rs index 364e3c28..a2c0e5a4 100644 --- a/iceoryx2-cli/iox2-nodes/src/main.rs +++ b/iceoryx2-cli/iox2-nodes/src/main.rs @@ -12,12 +12,12 @@ mod cli; mod commands; +mod filter; use clap::CommandFactory; use clap::Parser; use cli::Cli; use iceoryx2_bb_log::{set_log_level, LogLevel}; -use iceoryx2_cli_utils::Format; #[cfg(not(debug_assertions))] use human_panic::setup_panic; @@ -44,12 +44,17 @@ fn main() { Ok(cli) => { if let Some(action) = cli.action { match action { - cli::Action::List => { - if let Err(e) = commands::list(cli.format.unwrap_or(Format::Ron)) { + cli::Action::List(options) => { + if let Err(e) = commands::list(options.filter, cli.format) { eprintln!("Failed to list nodes: {}", e); } } - cli::Action::Details(_) => todo!(), + cli::Action::Details(options) => { + if let Err(e) = commands::details(options.node, options.filter, cli.format) + { + eprintln!("Failed to retrieve node details: {}", e); + } + } } } else { Cli::command().print_help().expect("Failed to print help"); diff --git a/iceoryx2-cli/iox2-services/src/commands.rs b/iceoryx2-cli/iox2-services/src/commands.rs index b96804ee..2b718b4f 100644 --- a/iceoryx2-cli/iox2-services/src/commands.rs +++ b/iceoryx2-cli/iox2-services/src/commands.rs @@ -10,7 +10,6 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::cli::DetailsFilter; use anyhow::{Context, Error, Result}; use iceoryx2::prelude::*; use iceoryx2_cli_utils::output::ServiceDescription; @@ -19,6 +18,8 @@ use iceoryx2_cli_utils::output::ServiceList; use iceoryx2_cli_utils::Filter; use iceoryx2_cli_utils::Format; +use crate::cli::DetailsFilter; + pub fn list(format: Format) -> Result<()> { let mut services = ServiceList::new(); diff --git a/iceoryx2-cli/utils/Cargo.toml b/iceoryx2-cli/utils/Cargo.toml index af869ad6..038e9237 100644 --- a/iceoryx2-cli/utils/Cargo.toml +++ b/iceoryx2-cli/utils/Cargo.toml @@ -14,6 +14,7 @@ version = { workspace = true } [dependencies] iceoryx2 = { workspace = true } +iceoryx2-pal-posix = {workspace = true} anyhow = { workspace = true } clap = { workspace = true } diff --git a/iceoryx2-cli/utils/src/output.rs b/iceoryx2-cli/utils/src/output.rs index 7c753c9d..c84da274 100644 --- a/iceoryx2-cli/utils/src/output.rs +++ b/iceoryx2-cli/utils/src/output.rs @@ -10,6 +10,9 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +use std::ops::Deref; + +use iceoryx2::node::NodeDetails as IceoryxNodeDetails; use iceoryx2::node::NodeId as IceoryxNodeId; use iceoryx2::node::NodeState as IceoryxNodeState; use iceoryx2::node::NodeView as IceoryxNodeView; @@ -18,6 +21,7 @@ use iceoryx2::service::static_config::messaging_pattern::MessagingPattern as Ice use iceoryx2::service::Service as IceoryxService; use iceoryx2::service::ServiceDetails as IceoryxServiceDetails; use iceoryx2::service::ServiceDynamicDetails as IceoryxServiceDynamicDetails; +use iceoryx2_pal_posix::posix::pid_t; #[derive(serde::Serialize, Eq, PartialEq, Ord, PartialOrd)] pub enum ServiceDescriptor { @@ -43,8 +47,69 @@ where } } +#[derive(serde::Serialize)] +pub struct ServiceDescription { + pub service_id: String, + pub service_name: String, + pub attributes: IceoryxAttributeSet, + pub pattern: IceoryxMessagingPattern, + pub nodes: Option, +} + +impl From<&IceoryxServiceDetails> for ServiceDescription +where + T: IceoryxService, +{ + fn from(service: &IceoryxServiceDetails) -> 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(NodeList::from), + } + } +} + pub type ServiceList = Vec; +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)] +pub struct NodeIdString(String); + +impl From<&IceoryxNodeId> for NodeIdString { + fn from(id: &IceoryxNodeId) -> Self { + NodeIdString(format!("{:032x}", id.value())) + } +} + +impl AsRef for NodeIdString { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for NodeIdString { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for NodeIdString { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl PartialEq<&str> for NodeIdString { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + #[derive(serde::Serialize)] pub enum NodeState { Alive, @@ -56,97 +121,120 @@ pub enum NodeState { #[derive(serde::Serialize)] pub struct NodeDescriptor { state: NodeState, - id: IceoryxNodeId, - name: Option, + id: NodeIdString, + pid: pid_t, executable: Option, + name: Option, } impl From<&IceoryxNodeState> for NodeDescriptor where T: IceoryxService, { - fn from(node_state: &IceoryxNodeState) -> Self { - match node_state { + fn from(node: &IceoryxNodeState) -> Self { + match node { IceoryxNodeState::Alive(view) => NodeDescriptor { state: NodeState::Alive, - id: *view.id(), - name: view - .details() - .as_ref() - .map(|details| details.name().as_str().to_string()), + id: NodeIdString::from(view.id()), + pid: view.id().pid().value(), executable: view .details() .as_ref() .map(|details| details.executable().to_string()), - }, - IceoryxNodeState::Dead(view) => NodeDescriptor { - state: NodeState::Dead, - id: *view.id(), name: view .details() .as_ref() .map(|details| details.name().as_str().to_string()), + }, + IceoryxNodeState::Dead(view) => NodeDescriptor { + state: NodeState::Dead, + id: NodeIdString::from(view.id()), + pid: view.id().pid().value(), executable: view .details() .as_ref() .map(|details| details.executable().to_string()), + name: view + .details() + .as_ref() + .map(|details| details.name().as_str().to_string()), }, IceoryxNodeState::Inaccessible(node_id) => NodeDescriptor { state: NodeState::Inaccessible, - id: *node_id, - name: None, + id: NodeIdString::from(node_id), + pid: node_id.pid().value(), executable: None, + name: None, }, IceoryxNodeState::Undefined(node_id) => NodeDescriptor { state: NodeState::Undefined, - id: *node_id, - name: None, + id: NodeIdString::from(node_id), + pid: node_id.pid().value(), executable: None, + name: None, }, } } } #[derive(serde::Serialize)] -pub struct NodeList { - pub num: usize, - pub details: Vec, +pub struct NodeDescription { + state: NodeState, + id: NodeIdString, + pid: pid_t, + #[serde(flatten)] + details: Option, } -impl From<&IceoryxServiceDynamicDetails> for NodeList +impl From<&IceoryxNodeState> for NodeDescription where T: IceoryxService, { - fn from(details: &IceoryxServiceDynamicDetails) -> Self { - NodeList { - num: details.nodes.len(), - details: details.nodes.iter().map(NodeDescriptor::from).collect(), + fn from(node: &IceoryxNodeState) -> Self { + match node { + IceoryxNodeState::Alive(view) => NodeDescription { + state: NodeState::Alive, + id: NodeIdString::from(view.id()), + pid: view.id().pid().value(), + details: view.details().clone(), + }, + IceoryxNodeState::Dead(view) => NodeDescription { + state: NodeState::Dead, + id: NodeIdString::from(view.id()), + pid: view.id().pid().value(), + details: view.details().clone(), + }, + IceoryxNodeState::Inaccessible(node_id) => NodeDescription { + state: NodeState::Inaccessible, + id: NodeIdString::from(node_id), + pid: node_id.pid().value(), + details: None, + }, + IceoryxNodeState::Undefined(node_id) => NodeDescription { + state: NodeState::Undefined, + id: NodeIdString::from(node_id), + pid: node_id.pid().value(), + details: None, + }, } } } #[derive(serde::Serialize)] -pub struct ServiceDescription { - pub service_id: String, - pub service_name: String, - pub attributes: IceoryxAttributeSet, - pub pattern: IceoryxMessagingPattern, - pub nodes: Option, +pub struct NodeList { + pub num: usize, + pub details: Vec, } -impl From<&IceoryxServiceDetails> for ServiceDescription +// @todo Remove +impl From<&IceoryxServiceDynamicDetails> for NodeList where T: IceoryxService, { - fn from(service: &IceoryxServiceDetails) -> 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(NodeList::from), + fn from(details: &IceoryxServiceDynamicDetails) -> Self { + NodeList { + num: details.nodes.len(), + details: details.nodes.iter().map(NodeDescriptor::from).collect(), } } }