diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a180225 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,48 @@ +name: Rust + +on: + push: + branches: + - main + + paths-ignore: + - '*.md' + + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + fail-fast: false + matrix: + rust: [stable, nightly] + + name: ${{ matrix.rust }} + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - name: cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --all + + - name: cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings --no-deps diff --git a/.gitignore b/.gitignore index ea8c4bf..cc8e3f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ +# Cargo /target + +# Plugins +/plugins +*.so diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5f02507 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "yaml.schemas": { + "https://json.schemastore.org/github-workflow.json": "file:///home/medzik/git/MedzikUser/rust/servers/.github/workflows/build.yml" + } +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 593e131..f7b5b74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,14 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +[[package]] +name = "plugin_test" +version = "0.1.0" +dependencies = [ + "async-trait", + "servers", +] + [[package]] name = "proc-macro2" version = "1.0.39" diff --git a/Cargo.toml b/Cargo.toml index 7d3d0c8..012748c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["plugin_test"] + [package] name = "servers" version = "0.1.0" diff --git a/plugin_test/Cargo.toml b/plugin_test/Cargo.toml new file mode 100644 index 0000000..d7e0b38 --- /dev/null +++ b/plugin_test/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "plugin_test" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["dylib"] + +[dependencies] +async-trait = "0.1.56" +servers = { path = ".." } diff --git a/plugin_test/src/lib.rs b/plugin_test/src/lib.rs new file mode 100644 index 0000000..ebc0d32 --- /dev/null +++ b/plugin_test/src/lib.rs @@ -0,0 +1,36 @@ +use async_trait::async_trait; +use servers::{Client, Command, CommandManagerType, CommandRegistrar, Plugin, PluginRegistrar}; + +struct PluginTest; + +#[async_trait] +impl Plugin for PluginTest { + fn name(&self) -> &'static str { + "test" + } + + async fn on_plugin_load(&self) {} + + async fn on_plugin_unload(&self) {} +} + +#[async_trait] +impl Command for PluginTest { + fn name(&self) -> &'static str { + "/test" + } + + fn help(&self) -> &'static str { + "test command" + } + + async fn execute(&self, client: &mut Client, _args: Vec<&str>, _commands: &CommandManagerType) { + client.send("content").await.expect("send message") + } +} + +#[no_mangle] +pub fn plugin_entry(registrar: &mut dyn PluginRegistrar, command: &mut dyn CommandRegistrar) { + registrar.register_plugin(Box::new(PluginTest)); + command.register_plugin(Box::new(PluginTest)); +} diff --git a/src/client.rs b/src/client.rs index 07bea65..79141ad 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,18 +17,28 @@ impl Client { /// Read message/buffer from Client pub async fn read(&mut self) -> anyhow::Result { + // allocate an empty buffer of length 1024 bytes let mut buf = [0; 1024]; + // read buffer from stream self.stream.read(&mut buf).await?; - let encoded = String::from_utf8(buf.to_vec())?.replace('\0', ""); + // encode &[u8] to a String and replace null spaces (empty `\0` bytes) + let decoded = String::from_utf8(buf.to_vec())?.replace('\0', ""); - Ok(encoded) + Ok(decoded) } /// Send message to Client pub async fn send(&mut self, content: &str) -> anyhow::Result<()> { - self.stream.write_all(format!("{content}\n\r").as_bytes()).await?; + // add a new line at the end of the content + let content = format!("{content}\n\r"); + + // send message + self.stream + .write_all(content.as_bytes()) + .await?; + Ok(()) } } diff --git a/src/command_handler.rs b/src/command_handler.rs index a3c32ad..104b3cc 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -2,21 +2,16 @@ use std::{any::Any, sync::Arc}; use async_trait::async_trait; -use crate::{Client, commands}; +use crate::Client; #[async_trait] pub trait Command: Any + Send + Sync { /// Command name fn name(&self) -> &'static str; - /// Command help message + /// Help message of this command fn help(&self) -> &'static str; /// Command function - async fn execute( - &self, - client: &mut Client, - args: Vec<&str>, - plugins: &CommandManagerType, - ); + async fn execute(&self, client: &mut Client, args: Vec<&str>, commands: &CommandManagerType); } pub struct CommandManager { @@ -38,20 +33,4 @@ impl Default for CommandManager { } } -pub type CommandManagerType = Vec>>; - -pub fn register_commands() -> CommandManagerType { - let mut command_manager = CommandManager::new(); - - for command in commands::register_commands() { - command_manager.commands.push(command) - } - - // create Arc in Vector - let mut commands: CommandManagerType = Vec::new(); - for command in command_manager.commands { - commands.push(Arc::new(command)) - } - - commands -} +pub type CommandManagerType = Arc>>; diff --git a/src/commands/help.rs b/src/commands/help.rs index c60c3dc..0a6591e 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -20,7 +20,7 @@ impl Command for CommandHelp { _args: Vec<&str>, commands: &CommandManagerType, ) { - for command in commands { + for command in commands.iter() { client .send(&format!("{} - {}", command.name(), command.help())) .await diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 483d1af..a37551e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,5 @@ +#![allow(clippy::vec_init_then_push)] + mod help; pub use help::*; diff --git a/src/main.rs b/src/main.rs index 11335b5..8358fe4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use servers::{loader, Client, PluginManagerType, register_commands, CommandManagerType}; +use servers::{loader, Client, CommandManagerType}; use tokio::{io::AsyncWriteExt, net::TcpListener}; #[tokio::main] @@ -6,24 +6,23 @@ async fn main() -> anyhow::Result<()> { // listen Tcp server let listener = TcpListener::bind("0.0.0.0:9999").await?; - // load plugins - let plugin_manager = loader()?; + println!("Tcp server started at: {}", listener.local_addr()?); - // load command - let commands_manager = register_commands(); + // load plugins and commands + let (_plugin_manager, commands_manager) = loader()?; // Accepts a new incoming connection from this listener. while let Ok((stream, _address)) = listener.accept().await { let client = Client::new(stream); // handle client connection in new thread - tokio::spawn(handle_connection(client, (*plugin_manager).to_vec(), (*commands_manager).to_vec())); + tokio::spawn(handle_connection(client, commands_manager.clone())); } Ok(()) } -async fn handle_connection(mut client: Client, plugins: PluginManagerType, commands: CommandManagerType) -> anyhow::Result<()> { +async fn handle_connection(mut client: Client, commands: CommandManagerType) -> anyhow::Result<()> { println!("New Client: {:?}", client.stream.peer_addr()?); loop { @@ -36,26 +35,23 @@ async fn handle_connection(mut client: Client, plugins: PluginManagerType, comma // get command from args let cmd = args[0]; - println!("{:?}", &args); - - for command in &commands { + // search if a command exists + for command in commands.iter() { + // if this is the entered command if cmd == command.name() { - println!("s"); - command.execute(&mut client, args[1..args.len()].to_vec(), &commands).await; - break - } - } + // execute command + command + .execute(&mut client, args[1..args.len()].to_vec(), &commands) + .await; - // search command in plugins - for plugin in &plugins { - // if command found execute plugin - if cmd == plugin.command() { - plugin.execute(&mut client, args[1..args.len()].to_vec()) + // don't search for more commands + break; } } // if an I/O or EOF error, abort the connection if client.stream.flush().await.is_err() { + // terminate connection break; } } diff --git a/src/plugin_loader.rs b/src/plugin_loader.rs index 104ad80..942f1bf 100644 --- a/src/plugin_loader.rs +++ b/src/plugin_loader.rs @@ -1,27 +1,22 @@ use std::{any::Any, fs, sync::Arc}; +use async_trait::async_trait; use libloading::{Library, Symbol}; use log::{debug, trace}; -use crate::Client; +use crate::{commands, Command, CommandManager, CommandManagerType}; /// A plugin which allows you to add extra functionality. +#[async_trait] pub trait Plugin: Any + Send + Sync { /// Get a name describing the `Plugin`. fn name(&self) -> &'static str; /// A function that runs immediately after plugin loading. /// Usually used for initialization. - fn on_plugin_load(&self); + async fn on_plugin_load(&self); /// A function that runs immediately before the plugin is unloaded. /// Use this if you want to do any cleanup. - fn on_plugin_unload(&self); - /// Plugin command. - /// For example: `/command` - fn command(&self) -> &'static str; - /// Help message of this command. - fn help(&self) -> &'static str; - /// The function will be executed, when sending plugin command. - fn execute(&self, client: &mut Client, args: Vec<&str>); + async fn on_plugin_unload(&self); } pub struct PluginManager { @@ -39,12 +34,12 @@ impl PluginManager { /// Unload all plugins and loaded plugin libraries, making sure to fire /// their `on_plugin_unload()` methods so they can do any necessary cleanup. - pub fn unload(&mut self) { + pub async fn unload(&mut self) { debug!("Unloading plugins"); for plugin in self.plugins.drain(..) { trace!("Firing on_plugin_unload for {:?}", plugin.name()); - plugin.on_plugin_unload(); + plugin.on_plugin_unload().await; } } } @@ -65,9 +60,19 @@ impl PluginRegistrar for PluginManager { } } -pub type PluginManagerType = Vec>>; +pub trait CommandRegistrar { + fn register_plugin(&mut self, command: Box); +} -pub fn loader() -> anyhow::Result { +impl CommandRegistrar for CommandManager { + fn register_plugin(&mut self, command: Box) { + self.commands.push(command) + } +} + +pub type PluginManagerType = Arc>>; + +pub fn loader() -> anyhow::Result<(PluginManagerType, CommandManagerType)> { // get path to .so lib from command argument let config_dir = "./plugins"; let paths = fs::read_dir(config_dir)?; @@ -75,6 +80,14 @@ pub fn loader() -> anyhow::Result { // create a plugin manager where all loaded plugins will be located let mut plugin_manager = PluginManager::new(); + // create a command manager where located all commands + let mut command_manager = CommandManager::new(); + + // register default commands + for command in commands::register_commands() { + command_manager.commands.push(command) + } + // for all plugin in directory for path in paths { // get library file path @@ -87,19 +100,26 @@ pub fn loader() -> anyhow::Result { let lib = Box::leak(Box::new(Library::new(path)?)); // get `plugin_entry` from library - let func: Symbol ()> = - lib.get(b"plugin_entry")?; + let func: Symbol< + unsafe extern "C" fn(&mut dyn PluginRegistrar, &mut dyn CommandRegistrar) -> (), + > = lib.get(b"plugin_entry")?; // execute initial function - func(&mut plugin_manager); + func(&mut plugin_manager, &mut command_manager); } } // create Arc in Vector - let mut plugins: PluginManagerType = Vec::new(); - for plugin in plugin_manager.plugins { - plugins.push(Arc::new(plugin)) + let mut commands = Vec::new(); + for command in command_manager.commands { + commands.push(command) } - Ok(plugins) + // create Arc in Vector + let mut plugins = Vec::new(); + for plugin in plugin_manager.plugins { + plugins.push(plugin) + } + + Ok((Arc::new(plugins), Arc::new(commands))) }