KillingSpark / rustysd

A service manager that is able to run "traditional" systemd services, written in rust
MIT License
495 stars 15 forks source link

[Offtopic] Busybox like applet structure / routing #45

Closed pwFoo closed 3 years ago

pwFoo commented 3 years ago

Hi @KillingSpark, I know the question is off topic to rustysd, but I try to start with rust and haven't found examples / documentation about how to build a busybox like binary...

I found out how to build a rust binary with optional features

#[cfg(feature = "shutdown")]
mod shutdown;

#[cfg(feature = "monitor")]
mod monitor;

Haven't understood directory structure, but got it work to execute function poweroff from shutdown mod inside of the main.rs file.

But... I don't like to hardcode subcommand calls to "applet functions" (rust modules)

poweroff => mod shutdown, execute function reboot
status => mod monitor, execute function status

Is there a way how to dynamically map subcommand to a module (or sub-crate or what else would make sense here)?

Searched such a application example structure without success. "Coreutils":https://github.com/uutils/coreutils have such a structure of modules, but how dispatching / routing of subcommand to applet / module is done?

Maybe you know an easy example / link which could help me to understand / build such a app base... or just close that issue because it's off topic.

Regards

bjorn3 commented 3 years ago

It seems that uutils uses a build script: https://github.com/uutils/coreutils/blob/0fa249a9449d842ab69e4e153e6ad5fdca15d6a5/build.rs

pwFoo commented 3 years ago

Hi @bjorn3, thanks for your update. I tried to find a design which could be automatically build / mapped by module / function naming and directory structure. Because optional feature need to be added in main.rs like that

#[cfg(feature = "shutdown")]
mod shutdown;

it would be ok to add here an additional line for mapping?

#[cfg(feature = "<applet>")]
mod <applet>;
use <applet>::<function> AS <subcommand>

But than I need to call a function like <subcommand>(args)? where subcommand is the first cli argument. Is that possible?

Additional question: Do you know a (german?) rust community (forum) where user help users with such questions? I would move off topic questions to an active community in the future.

bjorn3 commented 3 years ago

But than I need to call a function like (args)? where subcommand is the first cli argument. Is that possible?

rustysd uses pico-args for argument parsing. I haven't every used this crate, so unfortunately I can't help you.

Do you know a (german?) rust community (forum) where user help users with such questions? I would move off topic questions to an active community in the future.

You could join the (english) rust community discord: https://discord.com/invite/aVESxV8

pwFoo commented 3 years ago

Hi, I'm not related to pico_args, it's independent to rustysd.

bjorn3 commented 3 years ago

Are you using an existing argument parser? Clap for example has native support for subcommands. Or are you doing the argument parsing yourself? In that case you can match on the respective argument and call the function for the right subcommand.

pwFoo commented 3 years ago

pico_args support subcommands too, but how to call it dynamically? I don't want a static list of subcommand mapping. PHP have a call_user_func() method to call a function by variable value.

Is there a argument parser or something else which could do that based an a naming schema (subcommand => function)?

pwFoo commented 3 years ago

Played with match, std::env and I had some problems to understand how a simple string comarison works the rust way...

I built a hardcoded match with optional cases by feature mapping a simple subcommand to a module. With the current testing source code (see below) it's possible to add / remove mod / applet build time by features.

ToDo

src/main.rs

#[cfg(feature = "shutdown")]
mod shutdown;

#[cfg(feature = "monitor")]
mod monitor;

use std::env;

fn main() {
    if let arg1 = env::args().nth(1) {
        println!("{:?}", arg1);
        match arg1.as_deref() {
            #[cfg(feature = "shutdown")]
            Some("poweroff") => { 
                shutdown::shutdown::poweroff(); 
            }
            #[cfg(feature = "shutdown")]
            Some("reboot") => { 
                shutdown::shutdown::reboot(); 
            }
            #[cfg(feature = "monitor")]
            Some("status") => { 
                monitor::monitor::status(); 
            }
            // default case
            _ => {
                println!("Unknown or missing subcommand");
            }
        }
    }
}

src/shutdown/mod.rs

pub mod shutdown {
    pub fn reboot() {
        /*for argument in std::env::args() {
            println!("{:?}", argument);
        }*/
        println!("Rebooting now, see you!");
    }

    pub fn poweroff() {
        println!("Shutting down, bye!");
    }
}

src/monitor/mod.rs

pub mod monitor {
    pub fn status() {
        println!("Show monitoring status...");
    }
}
bjorn3 commented 3 years ago

Do I really write down all the options / subcommands? No way to generate a mapping like: <applet> <function> => <mod>::<function> (applet, command, additional options)

You can avoid it only using macros, which you may want to avoid for now as they are harder to learn.

is #[cfg(feature = "<applet>")] also needed inside of the mod.rs file to remove the unused mode from the binary? Binary size not changes because of the small mod file size...

No, if you use #[cfg(...)] on the mod item, there is no need to use it on every item in that module.

pwFoo commented 3 years ago

Added detection for symlink, but now match doesn't work anymore ... I have problems to understand types in rust...

29 |         match arg1 {
   |               ---- this expression has type `Option<&String>`
30 |             #[cfg(feature = "shutdown")]
31 |             Some("poweroff") => { 
   |                  ^^^^^^^^^^ expected struct `String`, found `str`

Happens because I changed from std::env::args to Vec<String> to modify cli args

let mut arguments: Vec<String> = std::env::args().collect();
[...]
    //if let arg1 = std::env::args().nth(1) {
    if let arg1 = arguments.get(1) {

I think I just use the "wrong" type for my needs?! Look at verification if symlink / applet called and modify cli args. Here is the main function...

fn main() {
    let mut arguments: Vec<String> = std::env::args().collect();
    // Check if applet called by symlink
    let script = std::env::args().nth(0).unwrap();
    let metadata = std::fs::symlink_metadata(&script).unwrap();
    // prepend binary file name
    if ! metadata.is_file() {                                                                               // is symlink, so an applet of binary is called!
        let file = std::fs::canonicalize(&script);                                                          // get called script absolute
        let calledScript = arguments.remove(0);                                                             // remove called script formatted like "./applet"
        let appletSymlinkPath = std::path::Path::new(&calledScript);                                        // create a Path object ?!
        arguments.insert(0, appletSymlinkPath.file_name().unwrap().to_os_string().into_string().unwrap());  // prepend applet without path as file_name only
        arguments.insert(0, file.unwrap().into_os_string().into_string().unwrap());                         // append binary absolute path instead of optional relative path
    }

   //if let arg1 = std::env::args().nth(1) {
    if let arg1 = arguments.get(1) {
        //match arg1.as_deref() {
        match arg1 {
            #[cfg(feature = "shutdown")]
            Some("poweroff") => { 
                shutdown::shutdown::poweroff(); 
            }
            #[cfg(feature = "shutdown")]
            Some("reboot") => { 
                shutdown::shutdown::reboot(); 
            }
            #[cfg(feature = "monitor")]
            Some("status") => { 
                monitor::monitor::status(); 
            }
            // default case
            _ => {
                println!("Unknown or missing subcommand");
            }
        }
    }
}
bjorn3 commented 3 years ago

Using match arg1.as_deref() { instead of match arg1 { should fix the type error. arg1.as_deref() turns the arg1 Option<String> into an Option<&str> that you can match on with string literals.

(this is getting quite off-topic. you should probably move this to the rust community discord or the rust user forum (users.rust-lang.org).)

pwFoo commented 3 years ago

Tried that before with as_deref(), but wasn't the solution. Fixed it with as_str() and remove Some() from cases...

Simple test my script works with applet as first argument and as symlink (= applet name) too.

#[cfg(feature = "shutdown")]
mod shutdown;

#[cfg(feature = "monitor")]
mod monitor;

fn main() {
    let mut arguments: Vec<String> = std::env::args().collect();
    // Check if applet called by symlink
    let script = std::env::args().nth(0).unwrap();
    let metadata = std::fs::symlink_metadata(&script).unwrap();
    // prepend binary file name
    if ! metadata.is_file() {                                                                               // is symlink, so an applet of binary is called!
        let file = std::fs::canonicalize(&script);                                                          // get called script absolute
        let calledScript = arguments.remove(0);                                                             // remove called script formatted like "./applet"
        let appletSymlinkPath = std::path::Path::new(&calledScript);                                        // create a Path object ?!
        arguments.insert(0, appletSymlinkPath.file_name().unwrap().to_os_string().into_string().unwrap());  // prepend applet without path as file_name only
        arguments.insert(0, file.unwrap().into_os_string().into_string().unwrap());                         // append binary absolute path instead of optional relative path
    }

    for arg in &arguments {
        println!("Argument {}", arg);
    }

    //if let arg1 = std::env::args().nth(1) {
    if let arg1 = &arguments[1] {
        //match arg1.as_deref() {
        //match arg1 {
        match arg1.as_str() {
            #[cfg(feature = "shutdown")]
            "poweroff" => { 
                shutdown::shutdown::poweroff(); 
            }
            #[cfg(feature = "shutdown")]
            "reboot" => { 
                shutdown::shutdown::reboot(); 
            }
            #[cfg(feature = "monitor")]
            "status" => { 
                monitor::monitor::status(); 
            }
            // default case
            _ => {
                println!("Unknown or missing subcommand");
            }
        }
    }
}
KillingSpark commented 3 years ago

Thanks @bjorn3 for helping out here. I would have gone the same way using match and #[cfg()].

For usability you might want to add a complete list of commands in the default case that checks if the command is a feature that has been disabled or if it is actually an unknown command.

If you want to go very fancy you can do something like git does and match the given command to existing ones and suggest the correct one. e.g.

#> git pusg
git: 'pusg' is not a git command. See 'git --help'.

The most similar command is
    push
KillingSpark commented 3 years ago

@pwFoo I can recommend the rust subreddit. It is english too but there are a lot of helpful people there that are willing to help newcomers get a hang on the language. You'll likely find some germans there too.

pwFoo commented 3 years ago

Hi @KillingSpark, maybe a nice feature. WOuld be nice to list all enabled features. Is it possible to generate a list of enabled features?

If you want to go very fancy you can do something like git does and match the given command to existing ones and suggest the correct one. e.g.

#> git pusg
git: 'pusg' is not a git command. See 'git --help'.

The most similar command is
  push
KillingSpark commented 3 years ago

Is it possible to generate a list of enabled features?

Not that I am aware of. But I am also not a big user of feature flags so there might be