From c34f0c3f652f507b547fc0ccd3156ed916dea2dc Mon Sep 17 00:00:00 2001 From: Loong <40141251+wangl-cc@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:26:47 +0000 Subject: [PATCH] WIP --- Cargo.lock | 146 +++++++++++++++++++++++++++++++ Cargo.toml | 2 + maa-cli/Cargo.toml | 2 + maa-rpc-api/Cargo.toml | 19 ++++ maa-rpc-api/README.md | 191 +++++++++++++++++++++++++++++++++++++++++ maa-rpc-api/src/lib.rs | 86 +++++++++++++++++++ 6 files changed, 446 insertions(+) create mode 100644 maa-rpc-api/Cargo.toml create mode 100644 maa-rpc-api/README.md create mode 100644 maa-rpc-api/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index faca0f64..2d3694fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,6 +1020,63 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpsee" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c71d8c1a731cc4227c2f698d377e7848ca12c8a48866fc5e6951c43a4db843" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-proc-macros", + "jsonrpsee-types", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2882f6f8acb9fdaec7cefc4fd607119a9bd709831df7d7672a1d3b644628280" +dependencies = [ + "async-trait", + "futures-util", + "http", + "jsonrpsee-types", + "parking_lot", + "rand", + "rustc-hash", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06c01ae0007548e73412c08e2285ffe5d723195bf268bce67b1b77c3bb2a14d" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a178c60086f24cc35bb82f57c651d0d25d99c4742b4d335de04e97fa1f08a8a1" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1105,6 +1162,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "lockfree-object-pool" version = "0.1.6" @@ -1116,6 +1183,9 @@ name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "serde", +] [[package]] name = "maa-cli" @@ -1135,8 +1205,10 @@ dependencies = [ "futures-util", "git2", "indicatif", + "jsonrpsee", "log", "maa-dirs", + "maa-rpc-api", "maa-sys", "maa-types", "prettytable", @@ -1167,6 +1239,18 @@ dependencies = [ "log", ] +[[package]] +name = "maa-rpc-api" +version = "0.1.0" +dependencies = [ + "chrono", + "jsonrpsee", + "log", + "maa-types", + "serde", + "serde_json", +] + [[package]] name = "maa-sys" version = "0.5.0" @@ -1297,6 +1381,29 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1349,6 +1456,15 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -1647,6 +1763,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.23" @@ -1973,9 +2095,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -2047,9 +2181,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.33" diff --git a/Cargo.toml b/Cargo.toml index 5555fbd8..0c41dc34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/MaaAssistantArknights/maa-cli" [workspace.dependencies] maa-dirs = { path = "maa-dirs", version = "0.1.1" } +maa-rpc-api = { path = "maa-rpc-api", version = "0.1" } maa-sys = { path = "maa-sys", version = "0.5.0" } maa-types = { path = "maa-types", version = "0.1" } @@ -27,6 +28,7 @@ flate2 = "1" futures-util = "0.3.28" git2 = "0.19" indicatif = "0.17.7" +jsonrpsee = "0.24.7" libloading = "0.8" log = "0.4.20" prettytable = { version = "0.10.0", default-features = false } diff --git a/maa-cli/Cargo.toml b/maa-cli/Cargo.toml index f38b4cb6..e81c73ca 100644 --- a/maa-cli/Cargo.toml +++ b/maa-cli/Cargo.toml @@ -39,6 +39,7 @@ path = "src/main.rs" [dependencies] maa-dirs = { workspace = true } +maa-rpc-api = { workspace = true, features = ["client", "server"] } maa-sys = { workspace = true, features = ["runtime"] } maa-types = { workspace = true, features = ["serde"] } @@ -56,6 +57,7 @@ flate2 = { workspace = true, optional = true } futures-util = { workspace = true, optional = true } git2 = { workspace = true, optional = true } indicatif = { workspace = true, optional = true } +jsonrpsee = { workspace = true } log = { workspace = true } prettytable = { workspace = true } reqwest = { workspace = true, features = ["blocking", "json"] } diff --git a/maa-rpc-api/Cargo.toml b/maa-rpc-api/Cargo.toml new file mode 100644 index 00000000..d9d5d76d --- /dev/null +++ b/maa-rpc-api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "maa-rpc-api" +version = "0.1.0" +edition = "2021" +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[features] +server = ["jsonrpsee/server-core"] +client = ["jsonrpsee/client-core"] + +[dependencies] +chrono = { workspace = true, default-features = false, features = ["serde"] } +jsonrpsee = { workspace = true, features = ["macros"] } +log = { workspace = true, features = ["serde"] } +maa-types = { workspace = true, features = ["serde"] } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/maa-rpc-api/README.md b/maa-rpc-api/README.md new file mode 100644 index 00000000..b277d9f9 --- /dev/null +++ b/maa-rpc-api/README.md @@ -0,0 +1,191 @@ +# maa-cli RPC API + +## Current Status + +Currently, maa-cli executes tasks through the following steps: + +1. Parse MaaCore related configuration (profile/\*.toml); +2. Parse tasks: read task definition files for custom tasks, process command line arguments for predefined tasks; +3. Modify partial configuration according to tasks; +4. Load MaaCore and initialize according to configuration; +5. Add parsed tasks to MaaCore and start; +6. Wait for tasks to complete and exit program. + +## Issues + +The above implementation is simple and straightforward, but has the following issues: + +- Loading and configuring MaaCore is required for each task execution, which takes some time (around a few seconds). This overhead can be ignored for longer tasks but becomes significant for simpler tasks, especially when executing multiple tasks consecutively via command line. +- Currently maa-cli has no external intervention methods after startup. Some MaaCore options allow runtime changes, but maa-cli's current implementation cannot support this. +- In the future, maa-cli can serve as a backend for other frontends to call. This way other frontends can avoid the relatively complex MaaCore FFI while also enabling WebUI development. + +## Solution + +Introduce a Server mode for maa-cli, started via `maa serve`. After startup, `maa-cli` will act as an RPC Server, listening on a Unix or TCP Socket. Commands like `maa run` and `maa startup` will parse tasks and send requests to the server. When running tasks, if the server is not started, maa-cli will start a server itself and keep it active as a daemon process in the background for a period of time. + +## Implementation Details + +- RPC Framework: JSON-RPC (crate jsonrpsee). Simple, easy to debug, natively supported by browsers, easy to integrate with existing code. RPC protocol related structs implemented in a separate crate. +- Transport: None or WebSocket or WS+TLS. WS is more efficient than HTTP, full-duplex, and like HTTP has native browser support. + +## RPC API List + +!!! Note +The following APIs are examples only, actual implementation may differ. JSON does not support comments, so comments in the JSON code below are for illustration only. + +RPC API is implemented using JSON-RPC 2.0. All requests and responses are in JSON format. See [JSON-RPC 2.0](https://www.jsonrpc.org/specification) for the JSON-RPC 2.0 specification. The basic structure is as follows: + +```json +{ + "jsonrpc": "2.0", // JSON-RPC version, must be "2.0" + "method": "AppendTask", // Method name, must be string, see below for specific method names + "params": {}, // Method parameters, optional, see below for specific parameters + "id": 1 // Request ID, optional, used to identify request, server will return same ID in response +} +``` + +### MaaCore Task Related + +#### Add Task + +Add a task to MaaCore. + +**Method Name**: `AsstAppendTask` + +**Method Parameters**: + +```jsonc +{ + "task_type": "StartUp", // Task type, see MAA integration docs for supported types + "task_params": {}, // Task parameters, see MAA integration docs for details + "process_task_params": false, // Whether to process task params, can be bool, string or string list. true processes all params, false processes none, string/string list processes specified param names +} +``` + +**Response Result**: + +```json +{ + "task_id": 1 // Task ID, integer to identify task, greater than 0 and less than int32 max value (2^31 - 1) +} +``` + +### Modify Task Parameters + +Modify parameters for a given task. MaaCore does not support removing tasks, but you can disable a task by setting its parameters to `{ "enabled": false }`. + +**Method Name**: `AsstSetParams` + +**Method Parameters**: + +```json +{ + "task_id": 1, // Task ID returned by `AsstAppendTask` + "task_params": {}, // New task parameters + "process_task_params": false // Whether to process task parameters, e.g. convert relative paths to absolute paths, defaults to false +} +``` + +**Response Result**: None + +### Start Tasks + +**Method Name**: `AsstStartTasks` + +**Method Parameters**: None + +**Response Result**: None + +### Stop Tasks + +Stop executing tasks (all incomplete tasks will be stopped). + +**Method Name**: `AsstStopTasks` + +**Method Parameters**: None + +**Response Result**: None + +### Check if Tasks are Running + +Check if any tasks are currently executing. + +**Method Name**: `AsstIsRunning` + +**Method Parameters**: None + +**Response Result**: + +```json +{ + "is_running": false // Whether tasks are currently executing +} +``` + +### Subscribe to Logs + +Subscribe to maa-cli logs. After subscribing, maa-cli will push logs to the client. + +**Method Name**: `SubscribeLog` + +**Method Parameters**: + +```json +{ + "level": "info" // Log level, see MAA integration docs for supported levels +} +``` + +## Server Control + +### Get Alive Time + +Get Server alive time. Server alive time is how long the Server will stay alive without any requests. Server will automatically close when alive time is reached. + +**Method Name**: `GetAliveTime` + +**Method Parameters**: None + +**Response Result**: + +```json +{ + "alive_time": 60 // Alive time in seconds +} +``` + +### Change Alive Time + +**Method Name**: `SetAliveTime` + +**Method Parameters**: + +```json +{ + "alive_time": 60 // Alive time in seconds +} +``` + +**Response Result**: None + +### Shutdown Server + +Shutdown Server. If tasks are running, this method will return an error. Please stop all tasks before shutting down Server. + +**Method Name**: `Terminate` + +**Method Parameters**: + +```json +{ + "wait_time": 60 // Wait time in seconds +} +``` + +**Response Result**: + +```json +{ + "time": "2021-01-01T00:00:00Z" // Expected shutdown time +} +``` diff --git a/maa-rpc-api/src/lib.rs b/maa-rpc-api/src/lib.rs new file mode 100644 index 00000000..f9222558 --- /dev/null +++ b/maa-rpc-api/src/lib.rs @@ -0,0 +1,86 @@ +use jsonrpsee::{core::SubscriptionResult, proc_macros::rpc, types::error::ErrorCode}; +use maa_types::{primitive::AsstTaskId, TaskType}; +use serde_json::Value; + +#[cfg_attr(all(feature = "server", not(feature = "client")), rpc(server))] +#[cfg_attr(all(feature = "client", not(feature = "server")), rpc(client))] +#[cfg_attr(all(feature = "client", feature = "server"), rpc(client, server))] +pub trait Rpc { + #[method(name = "load_core")] + /// Load (lib)MaaCore + /// + /// Currently, the path to the core can only by set at the server side for security reasons. + /// In the future, this method may be extended to allow clients to specify the path. + /// To make sure the library is trusted, the server may need to be signed by a trusted + /// key, and a public key should be specified at the server side. + async fn load_core(&self) -> Result<(), ErrorCode>; + + #[method(name = "unload_core")] + /// Unload (lib)MaaCore. + async fn unload_core(&self) -> Result<(), ErrorCode>; + + #[method(name = "set_log_dir")] + async fn set_log_dir(&self, log_dir: String) -> Result<(), ErrorCode>; + + #[method(name = "append_task")] + /// Append a task to task list. + async fn append_task( + &self, + task_type: TaskType, + task_params: Value, + process_params: Vec<&str>, + ) -> Result; + + #[method(name = "set_task_params")] + /// Set task parameters for task with given `task_id`. + async fn set_task_params( + &self, + task_id: AsstTaskId, + task_params: Value, + process_params: Vec<&str>, + ) -> Result<(), ErrorCode>; + + #[method(name = "start_tasks")] + /// Start task with given `id` + async fn start_task(&self, id: AsstTaskId) -> Result<(), ErrorCode>; + + #[method(name = "stop_tasks")] + async fn stop_tasks(&self) -> Result<(), ErrorCode>; + + #[method(name = "asst_state")] + /// Check if any task is running. + async fn asst_state(&self) -> Result; + + #[method(name = "log")] + /// Write a log message to server log. + fn log(&self, level: log::Level, message: &str) -> Result<(), ErrorCode>; + + #[subscription(name = "subscribe_log", item = LogMessage)] + /// Get log messages from server. + /// + /// If `raw` is `true`, return raw log messages. Otherwise, return log processed by server. + async fn subscribe_log(&self, raw: bool) -> SubscriptionResult; +} + +pub enum State { + /// MaaCore is not loaded. + Unloaded, + /// MaaCore is loaded but not initialized. + Uninitialized, + /// MaaCore is initialized but not connected to the device. + Unconnected, + /// MaaCore is available to run tasks. + Idle, + /// MaaCore is actively running tasks. + Running, +} + +#[derive(Debug)] +#[cfg_attr(feature = "client", derive(serde::Serialize))] +#[cfg_attr(feature = "server", derive(serde::Deserialize))] +/// A type representing a log message. +struct LogMessage { + timestamp: chrono::DateTime, + level: log::Level, + message: String, +}