From abc6eb6912fa8113ebe6238d95edbe920c703a30 Mon Sep 17 00:00:00 2001
From: huyubiao <huyubiao@huawei.com>
Date: Sun, 22 Oct 2023 14:42:13 +0800
Subject: [PATCH 004/103] feature(devctl): add devctl info

---
 exts/devmaster/src/bin/devctl/main.rs         |  91 ++-
 .../src/bin/devctl/subcmds/devctl_info.rs     | 680 ++++++++++++++++++
 .../src/bin/devctl/subcmds/devctl_utils.rs    |  29 +
 exts/devmaster/src/bin/devctl/subcmds/mod.rs  |   2 +
 exts/devmaster/src/lib/rules/node.rs          |   4 +-
 libs/basic/src/fd_util.rs                     |  26 +-
 libs/device/src/device.rs                     |  30 +-
 libs/device/src/device_enumerator.rs          |   2 +-
 8 files changed, 838 insertions(+), 26 deletions(-)
 create mode 100644 exts/devmaster/src/bin/devctl/subcmds/devctl_info.rs
 create mode 100644 exts/devmaster/src/bin/devctl/subcmds/devctl_utils.rs

diff --git a/exts/devmaster/src/bin/devctl/main.rs b/exts/devmaster/src/bin/devctl/main.rs
index 5fac776b..56d6d3b4 100644
--- a/exts/devmaster/src/bin/devctl/main.rs
+++ b/exts/devmaster/src/bin/devctl/main.rs
@@ -23,10 +23,13 @@ use log::init_log_to_console_syslog;
 use log::Level;
 use std::{io::Write, os::unix::net::UnixStream};
 use subcmds::devctl_hwdb::subcommand_hwdb;
+use subcmds::devctl_info::InfoArgs;
 use subcmds::devctl_monitor::subcommand_monitor;
 use subcmds::devctl_test_builtin::subcommand_test_builtin;
 use subcmds::devctl_trigger::subcommand_trigger;
 
+type Result<T> = std::result::Result<T, nix::Error>;
+
 /// parse program arguments
 #[derive(Parser, Debug)]
 #[clap(author, version, about, long_about = None)]
@@ -39,16 +42,63 @@ struct Args {
 /// Kinds of subcommands
 #[derive(Parser, Debug)]
 enum SubCmd {
-    /// Monitor device events from kernel and userspace
+    /// Query sysfs or the devmaster database
     #[clap(display_order = 1)]
+    Info {
+        #[clap(short, long, possible_values(&["name", "symlink", "path", "property", "env", "all"]), help(
+            "Query device information:\n\
+                name                     Name of device node\n\
+                symlink                  Pointing to node\n\
+                path                     sysfs device path\n\
+                property or env          The device properties\n\
+                all                      All values\n")
+        )]
+        query: Option<String>,
+
+        /// Print all key matches walking along the chain
+        /// of parent devices
+        #[clap(short, long)]
+        attribute_walk: bool,
+
+        /// Print major:minor of device containing this file
+        #[clap(short, long)]
+        device_id_of_file: Option<String>,
+
+        /// Export key/value pairs
+        #[clap(short('x'), long)]
+        export: bool,
+
+        /// Export the key name with a prefix
+        #[clap(short('P'), long)]
+        export_prefix: Option<String>,
+
+        /// Export the content of the devmaster database
+        #[clap(short('e'), long)]
+        export_db: bool,
+
+        /// Clean up the devmaster database
+        #[clap(short, long)]
+        cleanup_db: bool,
+
+        /// Prepend dev directory to path names
+        #[clap(short, long)]
+        root: bool,
+
+        ///
+        #[clap(required = false)]
+        devices: Vec<String>,
+    },
+
+    /// Monitor device events from kernel and userspace
+    #[clap(display_order = 2)]
     Monitor {},
 
     /// Kill all devmaster workers
-    #[clap(display_order = 2)]
+    #[clap(display_order = 3)]
     Kill {},
 
     /// Trigger a fake device action, then the kernel will report an uevent
-    #[clap(display_order = 3)]
+    #[clap(display_order = 4)]
     Trigger {
         /// the kind of device action to trigger
         #[clap(short, long)]
@@ -72,7 +122,7 @@ enum SubCmd {
     },
 
     /// Test builtin command on a device
-    #[clap(display_order = 4)]
+    #[clap(display_order = 5)]
     TestBuiltin {
         /// device action
         #[clap(short, long)]
@@ -85,7 +135,7 @@ enum SubCmd {
         syspath: String,
     },
     /// Test builtin command on a device
-    #[clap(display_order = 5)]
+    #[clap(display_order = 6)]
     Hwdb {
         /// update the hardware database
         #[clap(short('u'), long)]
@@ -114,16 +164,41 @@ fn subcommand_kill() {
     stream.write_all(b"kill ").unwrap();
 }
 
-fn main() {
+fn main() -> Result<()> {
     let argv: Vec<String> = std::env::args().collect();
     if invoked_as(argv, "devmaster") {
-        return run_daemon();
+        run_daemon();
+        return Ok(());
     }
 
     init_log_to_console_syslog("devctl", Level::Debug);
     let args = Args::parse();
 
     match args.subcmd {
+        SubCmd::Info {
+            query,
+            attribute_walk,
+            device_id_of_file,
+            export,
+            export_prefix,
+            export_db,
+            cleanup_db,
+            root,
+            devices,
+        } => {
+            return InfoArgs::new(
+                query,
+                attribute_walk,
+                device_id_of_file,
+                export,
+                export_prefix,
+                export_db,
+                cleanup_db,
+                root,
+                devices,
+            )
+            .subcommand_info()
+        }
         SubCmd::Monitor {} => subcommand_monitor(),
         SubCmd::Kill {} => subcommand_kill(),
         SubCmd::Trigger {
@@ -147,4 +222,6 @@ fn main() {
             root,
         } => subcommand_hwdb(update, test, path, usr, strict, root),
     }
+
+    Ok(())
 }
diff --git a/exts/devmaster/src/bin/devctl/subcmds/devctl_info.rs b/exts/devmaster/src/bin/devctl/subcmds/devctl_info.rs
new file mode 100644
index 00000000..0fa76751
--- /dev/null
+++ b/exts/devmaster/src/bin/devctl/subcmds/devctl_info.rs
@@ -0,0 +1,680 @@
+// Copyright (c) 2022 Huawei Technologies Co.,Ltd. All rights reserved.
+//
+// sysMaster is licensed under Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan
+// PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+//         http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
+// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+// See the Mulan PSL v2 for more details.
+
+//! subcommand for devctl trigger
+
+use crate::subcmds::devctl_utils;
+use basic::fd_util::{dot_or_dot_dot, xopendirat};
+use device::{device_enumerator::DeviceEnumerator, Device};
+use nix::dir::Dir;
+use nix::fcntl::{AtFlags, OFlag};
+use nix::sys::stat::fstatat;
+use nix::sys::stat::Mode;
+use nix::unistd::{unlinkat, UnlinkatFlags};
+use std::fs;
+use std::os::unix::fs::MetadataExt;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
+
+type Result<T> = std::result::Result<T, nix::Error>;
+
+#[derive(Debug)]
+enum QueryType {
+    Name,
+    Path,
+    Symlink,
+    Property,
+    All,
+}
+
+struct QueryProperty {
+    export: bool,
+    export_prefix: Option<String>,
+}
+
+impl QueryProperty {
+    fn new(export: bool, export_prefix: Option<String>) -> Self {
+        QueryProperty {
+            export,
+            export_prefix,
+        }
+    }
+}
+
+struct SysAttr {
+    name: String,
+    value: String,
+}
+
+pub struct InfoArgs {
+    query: Option<String>,
+    attribute_walk: bool,
+    device_id_of_file: Option<String>,
+    export: bool,
+    export_prefix: Option<String>,
+    export_db: bool,
+    cleanup_db: bool,
+    root: bool,
+    devices: Vec<String>,
+}
+
+impl InfoArgs {
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        query: Option<String>,
+        attribute_walk: bool,
+        device_id_of_file: Option<String>,
+        export: bool,
+        export_prefix: Option<String>,
+        export_db: bool,
+        cleanup_db: bool,
+        root: bool,
+        devices: Vec<String>,
+    ) -> Self {
+        InfoArgs {
+            query,
+            attribute_walk,
+            device_id_of_file,
+            export,
+            export_prefix,
+            export_db,
+            cleanup_db,
+            root,
+            devices,
+        }
+    }
+
+    /// subcommand for hwdb a fake device action, then the kernel will report an uevent
+    pub fn subcommand_info(&self) -> Result<()> {
+        let mut devs = Vec::new();
+
+        let mut arg_export = false;
+        if self.export || self.export_prefix.is_some() {
+            arg_export = true;
+        }
+
+        if self.export_db {
+            return export_devices();
+        }
+        if self.cleanup_db {
+            return cleanup_db();
+        }
+
+        if let Some(name) = self.device_id_of_file.as_ref() {
+            if !self.devices.is_empty() {
+                log::error!("Positional arguments are not allowed with -d/--device-id-of-file.");
+                return Err(nix::Error::EINVAL);
+            }
+            return self.stat_device(name, arg_export);
+        }
+
+        let mut query_type = QueryType::All;
+        self.parse_query_type(&mut query_type)?;
+
+        devs.extend(&self.devices);
+        if devs.is_empty() {
+            log::error!("A device name or path is required");
+            return Err(nix::Error::EINVAL);
+        }
+
+        if self.attribute_walk && devs.len() > 1 {
+            log::error!("Only one device may be specified with -a/--attribute-walk");
+            return Err(nix::Error::EINVAL);
+        }
+
+        let mut r: Result<()> = Ok(());
+        for dev in &self.devices {
+            let device = match devctl_utils::find_device(dev, "") {
+                Ok(d) => d,
+                Err(e) => {
+                    if e == nix::Error::EINVAL {
+                        log::error!("Bad argument {:?}, expected an absolute path in /dev/ or /sys/ or a unit name", dev);
+                    } else {
+                        log::error!("Unknown device {:?}", dev);
+                    }
+                    continue;
+                }
+            };
+
+            if self.query.is_some() {
+                let query_property = QueryProperty::new(arg_export, self.export_prefix.clone());
+                r = self.query_device(&query_type, device, query_property);
+            } else if self.attribute_walk {
+                r = print_device_chain(device);
+            } else {
+                log::error!("unknown action");
+                return Err(nix::Error::EINVAL);
+            }
+        }
+
+        r
+    }
+
+    fn parse_query_type(&self, query_type: &mut QueryType) -> Result<()> {
+        match &self.query {
+            Some(q) => {
+                if q == "property" || q == "env" {
+                    *query_type = QueryType::Property;
+                } else if q == "name" {
+                    *query_type = QueryType::Name;
+                } else if q == "symlink" {
+                    *query_type = QueryType::Symlink;
+                } else if q == "path" {
+                    *query_type = QueryType::Path;
+                } else if q == "all" {
+                    *query_type = QueryType::All;
+                } else {
+                    log::error!("unknown query type");
+                    return Err(nix::Error::EINVAL);
+                }
+            }
+            None => *query_type = QueryType::All,
+        }
+        Ok(())
+    }
+
+    fn stat_device(&self, name: &str, export: bool) -> Result<()> {
+        let metadata = match fs::metadata(name) {
+            Ok(metadata) => metadata,
+            Err(err) => {
+                log::error!("Failed to get metadata:{:?} err:{:?}", name, err);
+                return Err(nix::Error::EINVAL);
+            }
+        };
+
+        if export {
+            match &self.export_prefix {
+                Some(p) => {
+                    println!("{}MAJOR={}", p, nix::sys::stat::major(metadata.dev()));
+                    println!("{}MINOR={}", p, nix::sys::stat::minor(metadata.dev()));
+                }
+                None => {
+                    println!("INFO_MAJOR={}", nix::sys::stat::major(metadata.dev()));
+                    println!("INFO_MINOR={}", nix::sys::stat::minor(metadata.dev()));
+                }
+            }
+        } else {
+            println!(
+                "{}:{}",
+                nix::sys::stat::major(metadata.dev()),
+                nix::sys::stat::minor(metadata.dev())
+            );
+        }
+
+        Ok(())
+    }
+
+    fn query_device(
+        &self,
+        query: &QueryType,
+        device: Device,
+        property: QueryProperty,
+    ) -> Result<()> {
+        match query {
+            QueryType::Name => {
+                let node = match device.get_devname() {
+                    Ok(node) => node,
+                    Err(err) => {
+                        log::error!("No device node found");
+                        return Err(err.get_errno());
+                    }
+                };
+
+                if !self.root {
+                    println!(
+                        "{}",
+                        Path::new(&node)
+                            .strip_prefix("/dev/")
+                            .unwrap()
+                            .to_str()
+                            .unwrap()
+                    );
+                } else {
+                    println!("{}", node);
+                }
+                Ok(())
+            }
+            QueryType::Symlink => {
+                let mut devlinks_str = String::new();
+                for devlink in &device.devlink_iter() {
+                    if !self.root {
+                        devlinks_str += Path::new(&devlink)
+                            .strip_prefix("/dev/")
+                            .unwrap()
+                            .to_str()
+                            .unwrap();
+                    } else {
+                        devlinks_str += devlink;
+                    }
+                    devlinks_str += " ";
+                }
+                devlinks_str = devlinks_str.trim_end().to_string();
+
+                println!("{}", devlinks_str);
+                Ok(())
+            }
+            QueryType::Path => {
+                let devpath = match device.get_devpath() {
+                    Ok(devpath) => devpath,
+                    Err(err) => {
+                        log::error!("Failed to get device path");
+                        return Err(err.get_errno());
+                    }
+                };
+
+                println!("{}", devpath);
+                Ok(())
+            }
+            QueryType::Property => {
+                for (key, value) in &device.property_iter() {
+                    if property.export {
+                        match &property.export_prefix {
+                            Some(export_prefix) => println!("{}{}='{}'", export_prefix, key, value),
+                            None => println!("{}='{}'", key, value),
+                        }
+                    } else {
+                        println!("{}={}", key, value);
+                    }
+                }
+                Ok(())
+            }
+            QueryType::All => {
+                print_record(device, "");
+                Ok(())
+            }
+        }
+    }
+}
+
+fn print_device_chain(device: Device) -> Result<()> {
+    println!(
+        "\n\
+        Devctl info starts with the device specified by the devpath and then\n\
+        walks up the chain of parent devices. It prints for every device\n\
+        found, all possible attributes in the devmaster rules key format.\n\
+        A rule to match, can be composed by the attributes of the device\n\
+        and the attributes from one single parent device.\n\
+        "
+    );
+
+    print_all_attributes(&device, false)?;
+
+    let mut child = device;
+    while let Ok(parent) = child.get_parent() {
+        print_all_attributes(&parent.borrow(), true)?;
+
+        child = parent.borrow().clone();
+    }
+
+    Ok(())
+}
+
+fn print_all_attributes(device: &Device, is_parent: bool) -> Result<()> {
+    let mut devpath = String::from("");
+    let mut sysname = String::from("");
+    let mut subsystem = String::from("");
+    let mut driver = String::from("");
+
+    if let Ok(value) = device.get_devpath() {
+        devpath = value;
+    }
+
+    if let Ok(value) = device.get_sysname() {
+        sysname = value;
+    }
+
+    if let Ok(value) = device.get_subsystem() {
+        subsystem = value;
+    }
+
+    if let Ok(value) = device.get_driver() {
+        driver = value;
+    }
+
+    if is_parent {
+        println!("  looking at parent device '{}':", devpath);
+        println!("    KERNELS=={:?}", sysname);
+        println!("    SUBSYSTEMS=={:?}", subsystem);
+        println!("    DRIVERS=={:?}", driver);
+    } else {
+        println!("  looking at device '{}':", devpath);
+        println!("    KERNEL=={:?}", sysname);
+        println!("    SUBSYSTEM=={:?}", subsystem);
+        println!("    DRIVER=={:?}", driver);
+    }
+
+    let mut sysattrs: Vec<SysAttr> = Vec::new();
+
+    let iter = device.sysattr_iter();
+    for name in &iter {
+        if skip_attribute(name) {
+            continue;
+        }
+
+        let value = match device.get_sysattr_value(name) {
+            Ok(value) => {
+                /* skip any values that look like a path */
+                if value.starts_with('/') {
+                    continue;
+                }
+
+                /* skip nonprintable attributes */
+                if !value
+                    .chars()
+                    .all(|c| 0 != unsafe { libc::isprint(c as i32) })
+                {
+                    continue;
+                }
+
+                value
+            }
+            Err(e) => {
+                if e.get_errno() == nix::Error::EACCES || e.get_errno() == nix::Error::EPERM {
+                    "(not readable)".to_string()
+                } else {
+                    continue;
+                }
+            }
+        };
+
+        sysattrs.push(SysAttr {
+            name: name.to_string(),
+            value,
+        });
+    }
+
+    sysattrs.sort_by(|a, b| a.name.cmp(&b.name));
+
+    for sysattr in sysattrs {
+        if is_parent {
+            println!("    ATTRS{{{}}}=={:?}", sysattr.name, sysattr.value);
+        } else {
+            println!("    ATTR{{{}}}=={:?}", sysattr.name, sysattr.value);
+        }
+    }
+    println!();
+
+    Ok(())
+}
+
+fn skip_attribute(name: &str) -> bool {
+    /* Those are either displayed separately or should not be shown at all. */
+    if name.contains("uevent")
+        || name.contains("dev")
+        || name.contains("modalias")
+        || name.contains("resource")
+        || name.contains("driver")
+        || name.contains("subsystem")
+        || name.contains("module")
+    {
+        return true;
+    }
+
+    false
+}
+
+fn print_record(device: Device, prefix: &str) {
+    if let Ok(devpath) = device.get_devpath() {
+        println!("{}P: {}", prefix, devpath);
+    }
+
+    if let Ok(sysname) = device.get_sysname() {
+        println!("{}M: {}", prefix, sysname);
+    }
+
+    if let Ok(sysnum) = device.get_sysnum() {
+        println!("{}R: {}", prefix, sysnum);
+    }
+
+    let mut subsys = String::from("");
+    if let Ok(subsystem) = device.get_subsystem() {
+        subsys = subsystem.clone();
+        println!("{}U: {}", prefix, subsystem);
+    }
+
+    if let Ok(devnum) = device.get_devnum() {
+        if &subsys == "block" {
+            println!(
+                "{}D: b {}:{}",
+                prefix,
+                nix::sys::stat::major(devnum),
+                nix::sys::stat::minor(devnum)
+            );
+        } else {
+            println!(
+                "{}D: c {}:{}",
+                prefix,
+                nix::sys::stat::major(devnum),
+                nix::sys::stat::minor(devnum)
+            );
+        }
+    }
+
+    if let Ok(ifindex) = device.get_ifindex() {
+        println!("{}I: {}", prefix, ifindex);
+    }
+
+    if let Ok(devname) = device.get_devname() {
+        let val = Path::new(&devname)
+            .strip_prefix("/dev/")
+            .unwrap()
+            .to_str()
+            .unwrap();
+        println!("{}N: {}", prefix, val);
+        if let Ok(i) = device.get_devlink_priority() {
+            println!("{}L: {}", prefix, i);
+        }
+
+        for link in &device.devlink_iter() {
+            let val = Path::new(&link)
+                .strip_prefix("/dev/")
+                .unwrap()
+                .to_str()
+                .unwrap();
+            println!("{}S: {}", prefix, val);
+        }
+    }
+
+    if let Ok(q) = device.get_diskseq() {
+        println!("{}Q: {}", prefix, q);
+    }
+
+    if let Ok(driver) = device.get_driver() {
+        println!("{}V: {}", prefix, driver);
+    }
+
+    for (key, val) in &device.property_iter() {
+        println!("{}E: {}={}", prefix, key, val);
+    }
+
+    if prefix.is_empty() {
+        println!();
+    }
+}
+
+fn export_devices() -> Result<()> {
+    let mut e = DeviceEnumerator::new();
+
+    if let Err(err) = e.allow_uninitialized() {
+        log::error!("Failed to set allowing uninitialized flag");
+        return Err(err.get_errno());
+    }
+
+    if let Err(err) = e.scan_devices() {
+        log::error!("Failed to scan devices");
+        return Err(err.get_errno());
+    }
+
+    for device in e.iter() {
+        print_record(device.borrow().clone(), "");
+    }
+
+    Ok(())
+}
+
+fn cleanup_db() -> Result<()> {
+    if let Ok(mut dir1) = Dir::open("/run/devmaster/data", OFlag::O_DIRECTORY, Mode::empty()) {
+        cleanup_dir(&mut dir1, libc::S_ISVTX, 1);
+
+        if let Ok(mut dir2) = Dir::open("/run/devmaster/links", OFlag::O_DIRECTORY, Mode::empty()) {
+            cleanup_dirs_after_db_cleanup(&mut dir2, &dir1);
+        }
+
+        if let Ok(mut dir3) = Dir::open("/run/devmaster/tags", OFlag::O_DIRECTORY, Mode::empty()) {
+            cleanup_dirs_after_db_cleanup(&mut dir3, &dir1);
+        }
+    }
+
+    /* static_node-tags is not currently implemented */
+    if let Ok(mut dir) = Dir::open(
+        "/run/devmaster/static_node-tags",
+        OFlag::O_DIRECTORY,
+        Mode::empty(),
+    ) {
+        cleanup_dir(&mut dir, 0, 2);
+    }
+
+    /* Do not remove /run/devmaster/watch. It will be handled by devmaster well on restart.
+     * And should not be removed by external program when devmaster is running. */
+
+    Ok(())
+}
+
+fn cleanup_dir(dir: &mut Dir, mask: libc::mode_t, depth: i32) {
+    if depth <= 0 {
+        return;
+    }
+
+    let dir_raw_fd = dir.as_raw_fd();
+    for entry in dir.iter() {
+        let dent = match entry {
+            Ok(dent) => dent,
+            Err(_) => continue,
+        };
+
+        if dot_or_dot_dot(dent.file_name().to_str().unwrap()) {
+            continue;
+        }
+
+        let stats = match fstatat(dir_raw_fd, dent.file_name(), AtFlags::AT_SYMLINK_NOFOLLOW) {
+            Ok(stats) => stats,
+            Err(_) => continue,
+        };
+
+        if (stats.st_mode & mask) != 0 {
+            continue;
+        }
+
+        if stats.st_mode & libc::S_IFMT == libc::S_IFDIR {
+            match xopendirat(
+                dir_raw_fd,
+                dent.file_name().to_str().unwrap(),
+                OFlag::O_NOFOLLOW,
+            ) {
+                Ok(mut subdir) => cleanup_dir(&mut subdir, mask, depth - 1),
+                Err(e) => log::error!(
+                    "Failed to open subdirectory {:?}, err{:?}, ignoring",
+                    dent.file_name(),
+                    e
+                ),
+            }
+            let _ = unlinkat(Some(dir_raw_fd), dent.file_name(), UnlinkatFlags::RemoveDir);
+        } else {
+            let _ = unlinkat(
+                Some(dir_raw_fd),
+                dent.file_name(),
+                UnlinkatFlags::NoRemoveDir,
+            );
+        }
+    }
+}
+
+fn cleanup_dirs_after_db_cleanup(dir: &mut Dir, datadir: &Dir) {
+    let dir_raw_fd = dir.as_raw_fd();
+    for entry in dir.iter() {
+        let dent = match entry {
+            Ok(dent) => dent,
+            Err(_) => continue,
+        };
+
+        if dot_or_dot_dot(dent.file_name().to_str().unwrap()) {
+            continue;
+        }
+
+        let stats = match fstatat(dir_raw_fd, dent.file_name(), AtFlags::AT_SYMLINK_NOFOLLOW) {
+            Ok(stats) => stats,
+            Err(_) => continue,
+        };
+
+        if stats.st_mode & libc::S_IFMT == libc::S_IFDIR {
+            match xopendirat(
+                dir_raw_fd,
+                dent.file_name().to_str().unwrap(),
+                OFlag::O_NOFOLLOW,
+            ) {
+                Ok(mut subdir) => cleanup_dir_after_db_cleanup(&mut subdir, datadir),
+                Err(e) => log::error!(
+                    "Failed to open subdirectory {:?}, err{:?}, ignoring",
+                    dent.file_name(),
+                    e
+                ),
+            }
+            let _ = unlinkat(Some(dir_raw_fd), dent.file_name(), UnlinkatFlags::RemoveDir);
+        } else {
+            let _ = unlinkat(
+                Some(dir_raw_fd),
+                dent.file_name(),
+                UnlinkatFlags::NoRemoveDir,
+            );
+        }
+    }
+}
+
+/*
+ * Assume that dir is a directory with file names matching devmaster data base
+ * entries for devices in /run/devmaster/data (such as "b8:16"), and removes
+ * all files except those that haven't been deleted in /run/devmaster/data
+ * (i.e. they were skipped during db cleanup because of the db_persist flag).
+ */
+fn cleanup_dir_after_db_cleanup(dir: &mut Dir, datadir: &Dir) {
+    let dir_raw_fd = dir.as_raw_fd();
+    for entry in dir.iter() {
+        let dent = match entry {
+            Ok(dent) => dent,
+            Err(_) => continue,
+        };
+
+        if dot_or_dot_dot(dent.file_name().to_str().unwrap()) {
+            continue;
+        }
+
+        if unsafe {
+            libc::faccessat(
+                datadir.as_raw_fd(),
+                dent.file_name().as_ptr(),
+                libc::F_OK,
+                libc::AT_SYMLINK_NOFOLLOW,
+            )
+        } >= 0
+        {
+            /* The corresponding devmaster database file still exists.
+             * Assuming the persistent flag is set for the database. */
+            continue;
+        }
+
+        let _ = unlinkat(
+            Some(dir_raw_fd),
+            dent.file_name(),
+            UnlinkatFlags::NoRemoveDir,
+        );
+    }
+}
diff --git a/exts/devmaster/src/bin/devctl/subcmds/devctl_utils.rs b/exts/devmaster/src/bin/devctl/subcmds/devctl_utils.rs
new file mode 100644
index 00000000..63b7a98e
--- /dev/null
+++ b/exts/devmaster/src/bin/devctl/subcmds/devctl_utils.rs
@@ -0,0 +1,29 @@
+use device::Device;
+
+type Result<T> = std::result::Result<T, nix::Error>;
+
+/// find device by path or unit name
+pub fn find_device(id: &str, prefix: &str) -> Result<Device> {
+    if id.is_empty() {
+        return Err(nix::Error::EINVAL);
+    }
+    if let Ok(device) = Device::from_path(id) {
+        return Ok(device);
+    }
+
+    if !prefix.is_empty() && !id.starts_with(prefix) {
+        let path = prefix.to_string() + id;
+
+        if let Ok(device) = Device::from_path(&path) {
+            return Ok(device);
+        }
+    }
+
+    /* Check if the argument looks like a device unit name. */
+    find_device_from_unit(id)
+}
+
+/// dbus and device unit is not currently implemented
+fn find_device_from_unit(_unit_name: &str) -> Result<Device> {
+    todo!()
+}
diff --git a/exts/devmaster/src/bin/devctl/subcmds/mod.rs b/exts/devmaster/src/bin/devctl/subcmds/mod.rs
index e1e5ad37..3d79a88d 100644
--- a/exts/devmaster/src/bin/devctl/subcmds/mod.rs
+++ b/exts/devmaster/src/bin/devctl/subcmds/mod.rs
@@ -14,6 +14,8 @@
 //!
 
 pub(crate) mod devctl_hwdb;
+pub(crate) mod devctl_info;
 pub(crate) mod devctl_monitor;
 pub(crate) mod devctl_test_builtin;
 pub(crate) mod devctl_trigger;
+pub(self) mod devctl_utils;
diff --git a/exts/devmaster/src/lib/rules/node.rs b/exts/devmaster/src/lib/rules/node.rs
index 85005ed7..45b8b36b 100644
--- a/exts/devmaster/src/lib/rules/node.rs
+++ b/exts/devmaster/src/lib/rules/node.rs
@@ -35,7 +35,7 @@
 use crate::{error::*, log_dev, log_dev_option};
 use basic::fs_util::path_simplify;
 use basic::fs_util::{fchmod_and_chown, futimens_opath, symlink};
-use basic::{fd_util::opendirat, fs_util::remove_dir_until};
+use basic::{fd_util::xopendirat, fs_util::remove_dir_until};
 use cluFlock::ExclusiveFlock;
 use device::Device;
 use libc::{mode_t, S_IFBLK, S_IFCHR, S_IFLNK, S_IFMT};
@@ -584,7 +584,7 @@ pub(crate) fn find_prioritized_devnode(
     dev: Rc<RefCell<Device>>,
     dirfd: i32,
 ) -> Result<Option<String>> {
-    let mut dir = opendirat(dirfd, OFlag::O_NOFOLLOW)
+    let mut dir = xopendirat(dirfd, ".", OFlag::O_NOFOLLOW)
         .context(BasicSnafu)
         .log_error(&format!("failed to opendirat '{}'", dirfd))?;
 
diff --git a/libs/basic/src/fd_util.rs b/libs/basic/src/fd_util.rs
index 1e83ef8d..8715891c 100644
--- a/libs/basic/src/fd_util.rs
+++ b/libs/basic/src/fd_util.rs
@@ -14,6 +14,7 @@
 use crate::error::*;
 use libc::off_t;
 use nix::{
+    dir::Dir,
     errno::Errno,
     fcntl::{openat, FcntlArg, FdFlag, OFlag},
     ioctl_read,
@@ -160,11 +161,15 @@ pub fn fd_get_diskseq(fd: RawFd) -> Result<u64> {
     Ok(diskseq)
 }
 
-/// open the directory at fd
-pub fn opendirat(dirfd: i32, flags: OFlag) -> Result<nix::dir::Dir> {
+/// open the directory at dirfd
+pub fn xopendirat(dirfd: i32, name: &str, flags: OFlag) -> Result<nix::dir::Dir> {
+    if dirfd == libc::AT_FDCWD && flags.is_empty() {
+        return Dir::open(name, flags, Mode::empty()).context(NixSnafu);
+    }
+
     let nfd = openat(
         dirfd,
-        ".",
+        name,
         OFlag::O_RDONLY | OFlag::O_NONBLOCK | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC | flags,
         Mode::empty(),
     )
@@ -183,6 +188,10 @@ pub fn file_offset_beyond_memory_size(x: off_t) -> bool {
     x as u64 > usize::MAX as u64
 }
 
+/// "." or ".." directory
+pub fn dot_or_dot_dot(name: &str) -> bool {
+    name == "." || name == ".."
+}
 #[cfg(test)]
 mod tests {
     use crate::fd_util::{stat_is_char, stat_is_reg};
@@ -196,7 +205,7 @@ mod tests {
         path::Path,
     };
 
-    use super::opendirat;
+    use super::{dot_or_dot_dot, xopendirat};
 
     #[test]
     fn test_stats() {
@@ -228,7 +237,7 @@ mod tests {
         File::create("/tmp/test_opendirat/entry1").unwrap();
 
         let dirfd = open("/tmp/test_opendirat", OFlag::O_DIRECTORY, Mode::empty()).unwrap();
-        let mut dir = opendirat(dirfd, OFlag::O_NOFOLLOW).unwrap();
+        let mut dir = xopendirat(dirfd, ".", OFlag::O_NOFOLLOW).unwrap();
 
         for e in dir.iter() {
             let _ = e.unwrap();
@@ -236,4 +245,11 @@ mod tests {
 
         remove_dir_all("/tmp/test_opendirat").unwrap();
     }
+
+    #[test]
+    fn test_dot_or_dot_dot() {
+        assert!(dot_or_dot_dot("."));
+        assert!(dot_or_dot_dot(".."));
+        assert!(!dot_or_dot_dot("/"));
+    }
 }
diff --git a/libs/device/src/device.rs b/libs/device/src/device.rs
index fe1795c0..5b1eff1f 100644
--- a/libs/device/src/device.rs
+++ b/libs/device/src/device.rs
@@ -18,8 +18,8 @@ use crate::{error::*, DeviceAction};
 use basic::fs_util::{open_temporary, touch_file};
 use basic::parse::{device_path_parse_devnum, parse_devnum, parse_ifindex};
 use libc::{
-    c_char, dev_t, faccessat, gid_t, mode_t, uid_t, F_OK, S_IFBLK, S_IFCHR, S_IFDIR, S_IFLNK,
-    S_IFMT, S_IRUSR, S_IWUSR,
+    dev_t, faccessat, gid_t, mode_t, uid_t, F_OK, S_IFBLK, S_IFCHR, S_IFDIR, S_IFLNK, S_IFMT,
+    S_IRUSR, S_IWUSR,
 };
 use nix::dir::Dir;
 use nix::errno::{self, Errno};
@@ -30,6 +30,7 @@ use snafu::ResultExt;
 use std::cell::{Ref, RefCell};
 use std::collections::hash_set::Iter;
 use std::collections::{HashMap, HashSet, VecDeque};
+use std::ffi::CString;
 use std::fs::{self, rename, OpenOptions, ReadDir};
 use std::fs::{create_dir_all, File};
 use std::io::{Read, Write};
@@ -2458,13 +2459,13 @@ impl Device {
 
         let property_tags_outdated = *self.property_tags_outdated.borrow();
         if property_tags_outdated {
-            let all_tags: String = {
+            let mut all_tags: String = {
                 let all_tags = self.all_tags.borrow();
-                let tags_vec = all_tags.iter().map(|s| s.as_str()).collect::<Vec<_>>();
-                tags_vec.join(":")
+                all_tags.iter().map(|s| format!(":{}", s)).collect()
             };
 
             if !all_tags.is_empty() {
+                all_tags.push(':');
                 self.add_property_internal("TAGS", &all_tags)
                     .map_err(|e| Error::Nix {
                         msg: format!("properties_prepare failed: {}", e),
@@ -2472,13 +2473,13 @@ impl Device {
                     })?;
             }
 
-            let current_tags: String = {
+            let mut current_tags: String = {
                 let current_tags = self.current_tags.borrow();
-                let tags_vec = current_tags.iter().map(|s| s.as_str()).collect::<Vec<_>>();
-                tags_vec.join(":")
+                current_tags.iter().map(|s| format!(":{}", s)).collect()
             };
 
             if !current_tags.is_empty() {
+                current_tags.push(':');
                 self.add_property_internal("CURRENT_TAGS", &current_tags)
                     .map_err(|e| Error::Nix {
                         msg: format!("properties_prepare failed: {}", e),
@@ -2900,9 +2901,16 @@ impl Device {
         };
 
         if !subdir.is_empty() {
-            if unsafe { faccessat(dir.as_raw_fd(), "uevent".as_ptr() as *const c_char, F_OK, 0) }
-                >= 0
-            {
+            let uevent_str = match CString::new("uevent") {
+                Ok(uevent_str) => uevent_str,
+                Err(e) => {
+                    return Err(Error::Nix {
+                        msg: format!("failed to new CString({:?}) '{}'", "uevent", e),
+                        source: nix::Error::EINVAL,
+                    })
+                }
+            };
+            if unsafe { faccessat(dir.as_raw_fd(), uevent_str.as_ptr(), F_OK, 0) } >= 0 {
                 /* skip child device */
                 return Ok(());
             }
diff --git a/libs/device/src/device_enumerator.rs b/libs/device/src/device_enumerator.rs
index b0f98655..38956cf5 100644
--- a/libs/device/src/device_enumerator.rs
+++ b/libs/device/src/device_enumerator.rs
@@ -1143,7 +1143,7 @@ impl DeviceEnumerator {
     }
 
     /// scan devices
-    pub(crate) fn scan_devices(&mut self) -> Result<(), Error> {
+    pub fn scan_devices(&mut self) -> Result<(), Error> {
         if *self.scan_up_to_date.borrow() && *self.etype.borrow() == DeviceEnumerationType::Devices
         {
             return Ok(());
-- 
2.33.0

