├── .gitignore ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── .gitignore ├── misc.xml ├── modules.xml ├── boinc-rust-simple.iml └── git_toolbox_prj.xml ├── src ├── main.rs ├── util.rs ├── errors.rs ├── models.rs ├── rpc.rs └── lib.rs └── Cargo.toml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use tokio; 2 | use boinc_rpc; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let transport = 7 | boinc_rpc::Transport::new("127.0.0.1:31416", Some("ae6ae835752ca8a5b45ee05cea519541")); 8 | let mut client = boinc_rpc::Client::new(transport); 9 | 10 | println!("{:?}\n", client.get_account_manager_info().await.unwrap()); 11 | println!("{:?}\n", client.get_account_manager_info().await.unwrap()); 12 | } 13 | -------------------------------------------------------------------------------- /.idea/boinc-rust-simple.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boinc_rpc" 3 | version = "0.1.0" 4 | authors = ["apollo"] 5 | description = "Access BOINC clients via RPC" 6 | edition = "2018" 7 | repository = "https://github.com/vorot93/rust-boinc-rpc" 8 | keywords = [ 9 | "boinc", 10 | "rpc", 11 | "api", 12 | ] 13 | categories = [ 14 | "api-bindings", 15 | ] 16 | license = "Apache-2.0" 17 | 18 | [dependencies] 19 | bytes = "^1.1.0" 20 | encoding = "^0.2.33" 21 | futures = "^0.3.21" 22 | rust-crypto = "^0.2.36" 23 | tokio = { version = "^1.17.0", features = ["sync", "net", "rt-multi-thread"] } 24 | tokio-util = { version = "^0.7.1", features = ["codec"] } 25 | tower = { version="^0.4.12", features = ["util"] } 26 | tracing = "^0.1.32" 27 | treexml = "^0.7.0" 28 | 29 | [dev-dependencies] 30 | tokio = { version = "^1.17.0", features = ["macros"] } 31 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::errors::Error; 4 | 5 | pub fn parse_node(s: &str) -> Result { 6 | let doc = treexml::Document::parse(s.as_bytes())?; 7 | 8 | Ok(doc 9 | .root 10 | .ok_or_else(|| Error::NullError("Root is empty".into()))?) 11 | } 12 | 13 | pub fn eval_node_contents(node: &treexml::Element) -> Option 14 | where 15 | T: FromStr, 16 | { 17 | match node.text { 18 | Some(ref v) => v.parse::().ok(), 19 | _ => None, 20 | } 21 | } 22 | 23 | pub fn any_text(node: &treexml::Element) -> Option { 24 | if node.cdata.is_some() { 25 | return node.cdata.clone(); 26 | } 27 | if node.text.is_some() { 28 | return node.text.clone(); 29 | } 30 | None 31 | } 32 | 33 | pub fn trimmed_optional(e: &Option) -> Option { 34 | e.clone().map(|v| v.trim().into()) 35 | } 36 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, PartialEq, Debug)] 2 | pub enum Error { 3 | ConnectError(String), 4 | DataParseError(String), 5 | InvalidPasswordError(String), 6 | DaemonError(String), 7 | NullError(String), 8 | NetworkError(String), 9 | StatusError(i32), 10 | AuthError(String), 11 | InvalidURLError(String), 12 | AlreadyAttachedError(String), 13 | } 14 | 15 | impl From for Error { 16 | fn from(e: std::io::Error) -> Self { 17 | Self::NetworkError(format!("{}", e)) 18 | } 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: std::string::FromUtf8Error) -> Self { 23 | Self::DataParseError(format!("UTF-8 conversion error: {}", e.utf8_error())) 24 | } 25 | } 26 | 27 | impl From for Error { 28 | fn from(e: treexml::Error) -> Self { 29 | Self::DataParseError(format!("XML error: {}", e)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use super::util; 2 | 3 | #[derive(Clone, Copy, Debug)] 4 | pub enum Component { 5 | CPU, 6 | GPU, 7 | Network, 8 | } 9 | 10 | #[derive(Clone, Copy, Debug)] 11 | pub enum RunMode { 12 | Always, 13 | Auto, 14 | Never, 15 | Restore, 16 | } 17 | 18 | #[derive(Clone, Copy, Debug)] 19 | pub enum CpuSched { 20 | Uninitialized, 21 | Preempted, 22 | Scheduled, 23 | } 24 | 25 | #[derive(Clone, Copy, Debug)] 26 | pub enum ResultState { 27 | New, 28 | FilesDownloading, 29 | FilesDownloaded, 30 | ComputeError, 31 | FilesUploading, 32 | FilesUploaded, 33 | Aborted, 34 | UploadFailed, 35 | } 36 | 37 | #[derive(Clone, Copy, Debug)] 38 | pub enum Process { 39 | Uninitialized = 0, 40 | Executing = 1, 41 | Suspended = 9, 42 | AbortPending = 5, 43 | QuitPending = 8, 44 | CopyPending = 10, 45 | } 46 | 47 | #[derive(Clone, Debug, Default)] 48 | pub struct VersionInfo { 49 | pub major: Option, 50 | pub minor: Option, 51 | pub release: Option, 52 | } 53 | 54 | #[derive(Clone, Debug, Default)] 55 | pub struct HostInfo { 56 | pub tz_shift: Option, 57 | pub domain_name: Option, 58 | pub serialnum: Option, 59 | pub ip_addr: Option, 60 | pub host_cpid: Option, 61 | 62 | pub p_ncpus: Option, 63 | pub p_vendor: Option, 64 | pub p_model: Option, 65 | pub p_features: Option, 66 | pub p_fpops: Option, 67 | pub p_iops: Option, 68 | pub p_membw: Option, 69 | pub p_calculated: Option, 70 | pub p_vm_extensions_disabled: Option, 71 | 72 | pub m_nbytes: Option, 73 | pub m_cache: Option, 74 | pub m_swap: Option, 75 | 76 | pub d_total: Option, 77 | pub d_free: Option, 78 | 79 | pub os_name: Option, 80 | pub os_version: Option, 81 | pub product_name: Option, 82 | 83 | pub mac_address: Option, 84 | 85 | pub virtualbox_version: Option, 86 | } 87 | 88 | #[derive(Clone, Debug, Default)] 89 | pub struct ProjectInfo { 90 | pub name: Option, 91 | pub summary: Option, 92 | pub url: Option, 93 | pub general_area: Option, 94 | pub specific_area: Option, 95 | pub description: Option, 96 | pub home: Option, 97 | pub platforms: Option>, 98 | pub image: Option, 99 | } 100 | 101 | #[derive(Clone, Debug, Default)] 102 | pub struct AccountManagerInfo { 103 | pub url: Option, 104 | pub name: Option, 105 | pub have_credentials: Option, 106 | pub cookie_required: Option, 107 | pub cookie_failure_url: Option, 108 | } 109 | 110 | #[derive(Clone, Debug, Default)] 111 | pub struct Message { 112 | pub project_name: Option, 113 | pub priority: Option, 114 | pub msg_number: Option, 115 | pub body: Option, 116 | pub timestamp: Option, 117 | } 118 | 119 | #[derive(Clone, Debug, Default)] 120 | pub struct TaskResult { 121 | pub name: Option, 122 | pub wu_name: Option, 123 | pub platform: Option, 124 | pub version_num: Option, 125 | pub plan_class: Option, 126 | pub project_url: Option, 127 | pub final_cpu_time: Option, 128 | pub final_elapsed_time: Option, 129 | pub exit_status: Option, 130 | pub state: Option, 131 | pub report_deadline: Option, 132 | pub received_time: Option, 133 | pub estimated_cpu_time_remaining: Option, 134 | pub completed_time: Option, 135 | pub active_task: Option, 136 | } 137 | 138 | #[derive(Clone, Debug, Default)] 139 | pub struct ActiveTask { 140 | pub active_task_state: Option, 141 | pub app_version_num: Option, 142 | pub slot: Option, 143 | pub pid: Option, 144 | pub scheduler_state: Option, 145 | pub checkpoint_cpu_time: Option, 146 | pub fraction_done: Option, 147 | pub current_cpu_time: Option, 148 | pub elapsed_time: Option, 149 | pub swap_size: Option, 150 | pub working_set_size: Option, 151 | pub working_set_size_smoothed: Option, 152 | pub page_fault_rate: Option, 153 | pub bytes_sent: Option, 154 | pub bytes_received: Option, 155 | pub progress_rate: Option, 156 | } 157 | 158 | impl<'a> From<&'a treexml::Element> for ActiveTask { 159 | fn from(node: &treexml::Element) -> Self { 160 | let mut e = Self::default(); 161 | for n in &node.children { 162 | match &*n.name { 163 | "active_task_state" => { 164 | e.active_task_state = util::trimmed_optional(&n.text); 165 | } 166 | "app_version_num" => { 167 | e.app_version_num = util::trimmed_optional(&n.text); 168 | } 169 | "slot" => { 170 | e.slot = util::eval_node_contents(n); 171 | } 172 | "pid" => { 173 | e.pid = util::eval_node_contents(n); 174 | } 175 | "scheduler_state" => { 176 | e.scheduler_state = util::trimmed_optional(&n.text); 177 | } 178 | "checkpoint_cpu_time" => { 179 | e.checkpoint_cpu_time = util::eval_node_contents(n); 180 | } 181 | "fraction_done" => { 182 | e.fraction_done = util::eval_node_contents(n); 183 | } 184 | "current_cpu_time" => { 185 | e.current_cpu_time = util::eval_node_contents(n); 186 | } 187 | "elapsed_time" => { 188 | e.elapsed_time = util::eval_node_contents(n); 189 | } 190 | "swap_size" => { 191 | e.swap_size = util::eval_node_contents(n); 192 | } 193 | "working_set_size" => { 194 | e.working_set_size = util::eval_node_contents(n); 195 | } 196 | "working_set_size_smoothed" => { 197 | e.working_set_size_smoothed = util::eval_node_contents(n); 198 | } 199 | "page_fault_rate" => { 200 | e.page_fault_rate = util::eval_node_contents(n); 201 | } 202 | "bytes_sent" => { 203 | e.bytes_sent = util::eval_node_contents(n); 204 | } 205 | "bytes_received" => { 206 | e.bytes_received = util::eval_node_contents(n); 207 | } 208 | "progress_rate" => { 209 | e.progress_rate = util::eval_node_contents(n); 210 | } 211 | _ => {} 212 | } 213 | } 214 | e 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/rpc.rs: -------------------------------------------------------------------------------- 1 | extern crate crypto; 2 | 3 | use bytes::BytesMut; 4 | use crypto::digest::Digest; 5 | use encoding::{all::ISO_8859_1, DecoderTrap, EncoderTrap, Encoding}; 6 | use futures::{SinkExt, TryStreamExt}; 7 | use tokio::{ 8 | io::{AsyncRead, AsyncWrite}, 9 | net::TcpStream, 10 | }; 11 | use tokio_util::codec::{Decoder, Encoder, Framed}; 12 | use tracing::*; 13 | 14 | use crate::{errors::Error, util}; 15 | 16 | fn compute_nonce_hash(pass: &str, nonce: &str) -> String { 17 | let mut digest = crypto::md5::Md5::new(); 18 | digest.input_str(&format!("{}{}", nonce, pass)); 19 | digest.result_str() 20 | } 21 | 22 | const TERMCHAR: u8 = 3; 23 | 24 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 25 | pub enum CodecMode { 26 | Client, 27 | Server, 28 | } 29 | 30 | pub struct BoincCodec { 31 | mode: CodecMode, 32 | next_index: usize, 33 | } 34 | 35 | impl BoincCodec { 36 | #[must_use] 37 | pub const fn new(mode: CodecMode) -> Self { 38 | Self { 39 | mode, 40 | next_index: 0, 41 | } 42 | } 43 | } 44 | 45 | impl Decoder for BoincCodec { 46 | type Item = Vec; 47 | type Error = Error; 48 | 49 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 50 | let read_to = src.len(); 51 | 52 | if let Some(offset) = src[self.next_index..read_to] 53 | .iter() 54 | .position(|b| *b == TERMCHAR) 55 | { 56 | let newline_index = offset + self.next_index; 57 | self.next_index = 0; 58 | let line = src.split_to(newline_index + 1); 59 | let line = &line[..line.len() - 1]; 60 | let line = ISO_8859_1 61 | .decode(line, DecoderTrap::Strict) 62 | .map_err(|e| Error::DataParseError(format!("Invalid data received: {}", e)))?; 63 | 64 | trace!("Received data: {}", line); 65 | 66 | let line = line.trim_start_matches(""); 67 | let root_node = util::parse_node(line)?; 68 | 69 | let expected_root = match self.mode { 70 | CodecMode::Client => "boinc_gui_rpc_reply", 71 | CodecMode::Server => "boinc_gui_rpc_request", 72 | }; 73 | 74 | if root_node.name != expected_root { 75 | return Err(Error::DataParseError(format!( 76 | "Invalid root: {}. Expected: {}", 77 | root_node.name, expected_root 78 | ))); 79 | } 80 | 81 | Ok(Some(root_node.children)) 82 | } else { 83 | self.next_index = read_to; 84 | Ok(None) 85 | } 86 | } 87 | } 88 | 89 | impl Encoder> for BoincCodec { 90 | type Error = Error; 91 | 92 | fn encode( 93 | &mut self, 94 | item: Vec, 95 | dst: &mut BytesMut, 96 | ) -> Result<(), Self::Error> { 97 | let mut out = treexml::Element::new(match self.mode { 98 | CodecMode::Client => "boinc_gui_rpc_request", 99 | CodecMode::Server => "boinc_gui_rpc_reply", 100 | }); 101 | out.children = item; 102 | 103 | let data = format!("{}", out) 104 | .replace("", "") 105 | .replace(" />", "/>"); 106 | 107 | trace!("Sending data: {}", data); 108 | dst.extend_from_slice( 109 | &ISO_8859_1 110 | .encode(&data, EncoderTrap::Strict) 111 | .expect("Our data should always be correct"), 112 | ); 113 | dst.extend_from_slice(&[TERMCHAR]); 114 | Ok(()) 115 | } 116 | } 117 | 118 | pub struct DaemonStream { 119 | conn: Framed, 120 | } 121 | 122 | impl DaemonStream { 123 | pub async fn connect(host: String, password: Option) -> Result { 124 | Self::authenticate(TcpStream::connect(host).await?, password).await 125 | } 126 | } 127 | 128 | impl DaemonStream { 129 | async fn authenticate(io: Io, password: Option) -> Result { 130 | let mut conn = BoincCodec::new(CodecMode::Client).framed(io); 131 | 132 | let mut out = Some(vec![treexml::Element::new("auth1")]); 133 | 134 | let mut nonce_sent = false; 135 | loop { 136 | if let Some(data) = out.take() { 137 | conn.send(data).await?; 138 | 139 | let data = conn 140 | .try_next() 141 | .await? 142 | .ok_or_else(|| Error::DaemonError("EOF".into()))?; 143 | 144 | for node in data { 145 | match &*node.name { 146 | "nonce" => { 147 | if nonce_sent { 148 | return Err(Error::DaemonError( 149 | "Daemon requested nonce again - could be a bug".into(), 150 | )); 151 | } 152 | let mut nonce_node = treexml::Element::new("nonce_hash"); 153 | let pwd = password.clone().ok_or_else(|| { 154 | Error::AuthError("Password required for nonce".to_string()) 155 | })?; 156 | nonce_node.text = Some(compute_nonce_hash( 157 | &pwd, 158 | &node 159 | .text 160 | .ok_or_else(|| Error::AuthError("Invalid nonce".into()))?, 161 | )); 162 | 163 | let mut auth2_node = treexml::Element::new("auth2"); 164 | auth2_node.children.push(nonce_node); 165 | 166 | out = Some(vec![auth2_node]); 167 | nonce_sent = true; 168 | } 169 | "unauthorized" => { 170 | return Err(Error::AuthError("unauthorized".to_string())); 171 | } 172 | "error" => { 173 | return Err(Error::DaemonError(format!( 174 | "BOINC daemon returned error: {:?}", 175 | node.text 176 | ))); 177 | } 178 | "authorized" => { 179 | return Ok(Self { conn }); 180 | } 181 | _ => { 182 | return Err(Error::DaemonError(format!( 183 | "Invalid response from daemon: {}", 184 | node.name 185 | ))); 186 | } 187 | } 188 | } 189 | } else { 190 | return Err(Error::DaemonError("Empty response".into())); 191 | } 192 | } 193 | } 194 | 195 | pub(crate) async fn query( 196 | &mut self, 197 | request_data: Vec, 198 | ) -> Result, Error> { 199 | self.conn.send(request_data).await?; 200 | let data = self 201 | .conn 202 | .try_next() 203 | .await? 204 | .ok_or_else(|| Error::DaemonError("EOF".into()))?; 205 | 206 | Ok(data) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Rust client for BOINC RPC protocol. 2 | //! 3 | //! # Example 4 | //! 5 | //! ```rust,no_run 6 | //! # tokio::runtime::Runtime::new().unwrap().block_on(async { 7 | //! let transport = boinc_rpc::Transport::new("127.0.0.1:31416", Some("my-pass-in-gui_rpc_auth.cfg")); 8 | //! let mut client = boinc_rpc::Client::new(transport); 9 | //! 10 | //! println!("{:?}\n", client.get_messages(0).await.unwrap()); 11 | //! println!("{:?}\n", client.get_projects().await.unwrap()); 12 | //! println!("{:?}\n", client.get_account_manager_info().await.unwrap()); 13 | //! println!("{:?}\n", client.exchange_versions(&boinc_rpc::models::VersionInfo::default()).await.unwrap()); 14 | //! println!("{:?}\n", client.get_results(false).await.unwrap()); 15 | //! # }) 16 | //! ``` 17 | 18 | #![warn(clippy::all, clippy::pedantic, clippy::nursery)] 19 | #![allow(clippy::pub_enum_variant_names, clippy::type_complexity)] 20 | 21 | mod errors; 22 | pub mod models; 23 | pub mod rpc; 24 | mod util; 25 | 26 | use crate::{errors::*, rpc::*}; 27 | use std::{ 28 | fmt::Display, 29 | future::Future, 30 | pin::Pin, 31 | sync::Arc, 32 | task::{Context, Poll}, 33 | }; 34 | use tokio::{net::TcpStream, sync::Mutex}; 35 | use tower::ServiceExt; 36 | use treexml::Element; 37 | 38 | fn verify_rpc_reply_contents(data: &[treexml::Element]) -> Result { 39 | let mut success = false; 40 | for node in data { 41 | match &*node.name { 42 | "success" => success = true, 43 | "status" => { 44 | return Err(Error::StatusError( 45 | util::eval_node_contents(node).unwrap_or(9999), 46 | )); 47 | } 48 | "unauthorized" => { 49 | return Err(Error::AuthError(String::new())); 50 | } 51 | "error" => { 52 | let error_msg = node 53 | .text 54 | .clone() 55 | .ok_or_else(|| Error::DaemonError("Unknown error".into()))?; 56 | 57 | return match &*error_msg { 58 | "unauthorized" | "Missing authenticator" => Err(Error::AuthError(error_msg)), 59 | "Missing URL" => Err(Error::InvalidURLError(error_msg)), 60 | "Already attached to project" => Err(Error::AlreadyAttachedError(error_msg)), 61 | _ => Err(Error::DataParseError(error_msg)), 62 | }; 63 | } 64 | _ => {} 65 | } 66 | } 67 | Ok(success) 68 | } 69 | 70 | impl<'a> From<&'a treexml::Element> for models::Message { 71 | fn from(node: &treexml::Element) -> Self { 72 | let mut e = Self::default(); 73 | for n in &node.children { 74 | match &*n.name { 75 | "body" => { 76 | e.body = util::trimmed_optional(&n.cdata); 77 | } 78 | "project" => { 79 | e.project_name = util::trimmed_optional(&n.text); 80 | } 81 | "pri" => { 82 | e.priority = util::eval_node_contents(n); 83 | } 84 | "seqno" => { 85 | e.msg_number = util::eval_node_contents(n); 86 | } 87 | "time" => { 88 | e.timestamp = util::eval_node_contents(n); 89 | } 90 | _ => {} 91 | } 92 | } 93 | 94 | e 95 | } 96 | } 97 | 98 | impl<'a> From<&'a treexml::Element> for models::ProjectInfo { 99 | fn from(node: &treexml::Element) -> Self { 100 | let mut e = Self::default(); 101 | for n in &node.children { 102 | match &*n.name { 103 | "name" => { 104 | e.name = util::trimmed_optional(&util::any_text(n)); 105 | } 106 | "summary" => { 107 | e.summary = util::trimmed_optional(&util::any_text(n)); 108 | } 109 | "url" => { 110 | e.url = util::trimmed_optional(&util::any_text(n)); 111 | } 112 | "general_area" => { 113 | e.general_area = util::trimmed_optional(&util::any_text(n)); 114 | } 115 | "specific_area" => { 116 | e.specific_area = util::trimmed_optional(&util::any_text(n)); 117 | } 118 | "description" => { 119 | e.description = util::trimmed_optional(&util::any_text(n)); 120 | } 121 | "home" => { 122 | e.home = util::trimmed_optional(&util::any_text(n)); 123 | } 124 | "platfroms" => { 125 | let mut platforms = Vec::new(); 126 | for platform_node in &n.children { 127 | if platform_node.name == "platform" { 128 | if let Some(v) = &platform_node.text { 129 | platforms.push(v.clone()); 130 | } 131 | } 132 | } 133 | e.platforms = Some(platforms); 134 | } 135 | "image" => { 136 | e.image = util::trimmed_optional(&util::any_text(n)); 137 | } 138 | _ => {} 139 | } 140 | } 141 | 142 | e 143 | } 144 | } 145 | 146 | impl<'a> From<&'a treexml::Element> for models::AccountManagerInfo { 147 | fn from(node: &treexml::Element) -> Self { 148 | let mut e = Self::default(); 149 | for n in &node.children { 150 | match &*n.name { 151 | "acct_mgr_url" => e.url = util::trimmed_optional(&util::any_text(n)), 152 | "acct_mgr_name" => e.name = util::trimmed_optional(&util::any_text(n)), 153 | "have_credentials" => { 154 | e.have_credentials = Some(true); 155 | } 156 | "cookie_required" => { 157 | e.cookie_required = Some(true); 158 | } 159 | "cookie_failure_url" => { 160 | e.cookie_failure_url = util::trimmed_optional(&util::any_text(n)) 161 | } 162 | _ => {} 163 | } 164 | } 165 | e 166 | } 167 | } 168 | 169 | impl<'a> From<&'a treexml::Element> for models::VersionInfo { 170 | fn from(node: &treexml::Element) -> Self { 171 | let mut e = Self::default(); 172 | for n in &node.children { 173 | match &*n.name { 174 | "major" => e.major = util::eval_node_contents(n), 175 | "minor" => e.minor = util::eval_node_contents(n), 176 | "release" => e.release = util::eval_node_contents(n), 177 | _ => {} 178 | } 179 | } 180 | e 181 | } 182 | } 183 | 184 | impl<'a> From<&'a treexml::Element> for models::TaskResult { 185 | fn from(node: &treexml::Element) -> Self { 186 | let mut e = Self::default(); 187 | for n in &node.children { 188 | match &*n.name { 189 | "name" => { 190 | e.name = util::trimmed_optional(&n.text); 191 | } 192 | "wu_name" => { 193 | e.wu_name = util::trimmed_optional(&n.text); 194 | } 195 | "platform" => { 196 | e.platform = util::trimmed_optional(&n.text); 197 | } 198 | "version_num" => { 199 | e.version_num = util::eval_node_contents(n); 200 | } 201 | "plan_class" => { 202 | e.plan_class = util::trimmed_optional(&n.text); 203 | } 204 | "project_url" => { 205 | e.project_url = util::trimmed_optional(&n.text); 206 | } 207 | "final_cpu_time" => { 208 | e.final_cpu_time = util::eval_node_contents(n); 209 | } 210 | "final_elapsed_time" => { 211 | e.final_elapsed_time = util::eval_node_contents(n); 212 | } 213 | "exit_status" => { 214 | e.exit_status = util::eval_node_contents(n); 215 | } 216 | "state" => { 217 | e.state = util::eval_node_contents(n); 218 | } 219 | "report_deadline" => { 220 | e.report_deadline = util::eval_node_contents(n); 221 | } 222 | "received_time" => { 223 | e.received_time = util::eval_node_contents(n); 224 | } 225 | "estimated_cpu_time_remaining" => { 226 | e.estimated_cpu_time_remaining = util::eval_node_contents(n); 227 | } 228 | "completed_time" => { 229 | e.completed_time = util::eval_node_contents(n); 230 | } 231 | "active_task" => { 232 | e.active_task = Some(models::ActiveTask::from(n)); 233 | } 234 | _ => {} 235 | } 236 | } 237 | e 238 | } 239 | } 240 | 241 | impl<'a> From<&'a treexml::Element> for models::HostInfo { 242 | fn from(node: &treexml::Element) -> Self { 243 | let mut e = Self::default(); 244 | for n in &node.children { 245 | match &*n.name { 246 | "p_fpops" => e.p_fpops = util::eval_node_contents(n), 247 | "p_iops" => e.p_iops = util::eval_node_contents(n), 248 | "p_membw" => e.p_membw = util::eval_node_contents(n), 249 | "p_calculated" => e.p_calculated = util::eval_node_contents(n), 250 | "p_vm_extensions_disabled" => { 251 | e.p_vm_extensions_disabled = util::eval_node_contents(n) 252 | } 253 | "host_cpid" => e.host_cpid = n.text.clone(), 254 | "product_name" => e.product_name = n.text.clone(), 255 | "mac_address" => e.mac_address = n.text.clone(), 256 | "domain_name" => e.domain_name = n.text.clone(), 257 | "ip_addr" => e.ip_addr = n.text.clone(), 258 | "p_vendor" => e.p_vendor = n.text.clone(), 259 | "p_model" => e.p_model = n.text.clone(), 260 | "os_name" => e.os_name = n.text.clone(), 261 | "os_version" => e.os_version = n.text.clone(), 262 | "virtualbox_version" => e.virtualbox_version = n.text.clone(), 263 | "p_features" => e.p_features = n.text.clone(), 264 | "timezone" => e.tz_shift = util::eval_node_contents(n), 265 | "p_ncpus" => e.p_ncpus = util::eval_node_contents(n), 266 | "m_nbytes" => e.m_nbytes = util::eval_node_contents(n), 267 | "m_cache" => e.m_cache = util::eval_node_contents(n), 268 | "m_swap" => e.m_swap = util::eval_node_contents(n), 269 | "d_total" => e.d_total = util::eval_node_contents(n), 270 | "d_free" => e.d_free = util::eval_node_contents(n), 271 | _ => {} 272 | } 273 | } 274 | e 275 | } 276 | } 277 | 278 | type DaemonStreamFuture = 279 | Pin, Error>> + Send + Sync + 'static>>; 280 | 281 | enum ConnState { 282 | Connecting(DaemonStreamFuture), 283 | Ready(DaemonStream), 284 | Error(Error), 285 | } 286 | 287 | pub struct Transport { 288 | state: Arc>>, 289 | } 290 | 291 | impl Transport { 292 | pub fn new(addr: A, password: Option

) -> Self { 293 | let addr = addr.to_string(); 294 | let password = password.map(|p| p.to_string()); 295 | Self { 296 | state: Arc::new(Mutex::new(Some(ConnState::Connecting(Box::pin( 297 | DaemonStream::connect(addr, password), 298 | ))))), 299 | } 300 | } 301 | } 302 | 303 | impl tower::Service> for Transport { 304 | type Response = Vec; 305 | type Error = Error; 306 | type Future = Pin>>>; 307 | 308 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 309 | let mut g = match self.state.try_lock() { 310 | Ok(g) => g, 311 | Err(e) => { return Poll::Pending; } 312 | }; 313 | 314 | let (state, out) = match g.take().unwrap() { 315 | ConnState::Connecting(mut future) => { 316 | let res = future.as_mut().poll(cx); 317 | match res { 318 | Poll::Pending => (Some(ConnState::Connecting(future)), Poll::Pending), 319 | Poll::Ready(Ok(conn)) => (Some(ConnState::Ready(conn)), Poll::Ready(Ok(()))), 320 | Poll::Ready(Err(e)) => (None, Poll::Ready(Err(e))), 321 | } 322 | } 323 | ConnState::Ready(conn) => (Some(ConnState::Ready(conn)), Poll::Ready(Ok(()))), 324 | ConnState::Error(error) => ( 325 | Some(ConnState::Error(error.clone())), 326 | Poll::Ready(Err(error)), 327 | ), 328 | }; 329 | 330 | *g = state; 331 | out 332 | } 333 | 334 | fn call(&mut self, req: Vec) -> Self::Future { 335 | let state = self.state.clone(); 336 | Box::pin(async move { 337 | let mut state = state.lock().await; 338 | let mut conn = match state.take() { 339 | Some(ConnState::Ready(conn)) => conn, 340 | _ => unreachable!(), 341 | }; 342 | let query_res = conn.query(req).await; 343 | if let Err(e) = &query_res { 344 | *state = Some(ConnState::Error(e.clone())); 345 | } else { 346 | *state = Some(ConnState::Ready(conn)); 347 | } 348 | query_res 349 | }) 350 | } 351 | } 352 | 353 | pub struct Client { 354 | transport: S, 355 | } 356 | 357 | impl Client 358 | where 359 | S: tower::Service, Response=Vec, Error=Error>, 360 | { 361 | pub fn new(transport: S) -> Self { 362 | Self { transport } 363 | } 364 | 365 | async fn get_object From<&'a treexml::Element>>( 366 | &mut self, 367 | req_data: Vec, 368 | object_tag: &str, 369 | ) -> Result { 370 | self.transport.ready().await?; 371 | let data = self.transport.call(req_data).await?; 372 | verify_rpc_reply_contents(&data)?; 373 | for child in &data { 374 | if child.name == object_tag { 375 | return Ok(T::from(child)); 376 | } 377 | } 378 | Err(Error::DataParseError("Object not found.".to_string())) 379 | } 380 | 381 | async fn get_object_by_req_tag From<&'a treexml::Element>>( 382 | &mut self, 383 | req_tag: &str, 384 | object_tag: &str, 385 | ) -> Result { 386 | self.get_object(vec![treexml::Element::new(req_tag)], object_tag) 387 | .await 388 | } 389 | 390 | async fn get_vec From<&'a treexml::Element>>( 391 | &mut self, 392 | req_data: Vec, 393 | vec_tag: &str, 394 | object_tag: &str, 395 | ) -> Result, Error> { 396 | let mut v = Vec::new(); 397 | { 398 | self.transport.ready().await?; 399 | let data = self.transport.call(req_data).await?; 400 | verify_rpc_reply_contents(&data)?; 401 | let mut success = false; 402 | for child in data { 403 | if child.name == vec_tag { 404 | success = true; 405 | for vec_child in &child.children { 406 | if vec_child.name == object_tag { 407 | v.push(T::from(vec_child)); 408 | } 409 | } 410 | } 411 | } 412 | if !success { 413 | return Err(Error::DataParseError("Objects not found.".to_string())); 414 | } 415 | } 416 | Ok(v) 417 | } 418 | 419 | async fn get_vec_by_req_tag From<&'a treexml::Element>>( 420 | &mut self, 421 | req_tag: &str, 422 | vec_tag: &str, 423 | object_tag: &str, 424 | ) -> Result, Error> { 425 | self.get_vec(vec![treexml::Element::new(req_tag)], vec_tag, object_tag) 426 | .await 427 | } 428 | 429 | pub async fn get_messages(&mut self, seqno: i64) -> Result, Error> { 430 | self.get_vec( 431 | vec![{ 432 | let mut node = treexml::Element::new("get_messages"); 433 | node.text = Some(format!("{}", seqno)); 434 | node 435 | }], 436 | "msgs", 437 | "msg", 438 | ) 439 | .await 440 | } 441 | 442 | pub async fn get_projects(&mut self) -> Result, Error> { 443 | self.get_vec_by_req_tag("get_all_projects_list", "projects", "project") 444 | .await 445 | } 446 | 447 | pub async fn get_account_manager_info(&mut self) -> Result { 448 | self.get_object_by_req_tag("acct_mgr_info", "acct_mgr_info") 449 | .await 450 | } 451 | 452 | pub async fn get_account_manager_rpc_status(&mut self) -> Result { 453 | self.transport.ready().await?; 454 | let data = self 455 | .transport 456 | .call(vec![treexml::Element::new("acct_mgr_rpc_poll")]) 457 | .await?; 458 | verify_rpc_reply_contents(&data)?; 459 | 460 | let mut v: Option = None; 461 | for child in &data { 462 | if &*child.name == "acct_mgr_rpc_reply" { 463 | for c in &child.children { 464 | if &*c.name == "error_num" { 465 | v = util::eval_node_contents(c); 466 | } 467 | } 468 | } 469 | } 470 | v.ok_or_else(|| Error::DataParseError("acct_mgr_rpc_reply node not found".into())) 471 | } 472 | 473 | pub async fn connect_to_account_manager( 474 | &mut self, 475 | url: &str, 476 | name: &str, 477 | password: &str, 478 | ) -> Result { 479 | let mut req_node = treexml::Element::new("acct_mgr_rpc"); 480 | req_node.children = vec![ 481 | { 482 | let mut node = treexml::Element::new("url"); 483 | node.text = Some(url.into()); 484 | node 485 | }, 486 | { 487 | let mut node = treexml::Element::new("name"); 488 | node.text = Some(name.into()); 489 | node 490 | }, 491 | { 492 | let mut node = treexml::Element::new("password"); 493 | node.text = Some(password.into()); 494 | node 495 | }, 496 | ]; 497 | self.transport.ready().await?; 498 | let root_node = self.transport.call(vec![req_node]).await?; 499 | Ok(verify_rpc_reply_contents(&root_node)?) 500 | } 501 | 502 | pub async fn exchange_versions( 503 | &mut self, 504 | info: &models::VersionInfo, 505 | ) -> Result { 506 | let mut content_node = treexml::Element::new("exchange_versions"); 507 | { 508 | let mut node = treexml::Element::new("major"); 509 | node.text = info.minor.map(|v| format!("{}", v)); 510 | content_node.children.push(node); 511 | } 512 | { 513 | let mut node = treexml::Element::new("minor"); 514 | node.text = info.major.map(|v| format!("{}", v)); 515 | content_node.children.push(node); 516 | } 517 | { 518 | let mut node = treexml::Element::new("release"); 519 | node.text = info.release.map(|v| format!("{}", v)); 520 | content_node.children.push(node); 521 | } 522 | self.get_object(vec![content_node], "server_version").await 523 | } 524 | 525 | pub async fn get_results( 526 | &mut self, 527 | active_only: bool, 528 | ) -> Result, Error> { 529 | self.get_vec( 530 | vec![{ 531 | let mut node = treexml::Element::new("get_results"); 532 | if active_only { 533 | let mut ao_node = treexml::Element::new("active_only"); 534 | ao_node.text = Some("1".into()); 535 | node.children.push(ao_node); 536 | } 537 | node 538 | }], 539 | "results", 540 | "result", 541 | ) 542 | .await 543 | } 544 | 545 | pub async fn set_mode( 546 | &mut self, 547 | c: models::Component, 548 | m: models::RunMode, 549 | duration: f64, 550 | ) -> Result<(), Error> { 551 | self.transport.ready().await?; 552 | let rsp_root = self 553 | .transport 554 | .call(vec![{ 555 | let comp_desc = match c { 556 | models::Component::CPU => "run", 557 | models::Component::GPU => "gpu", 558 | models::Component::Network => "network", 559 | } 560 | .to_string(); 561 | let mode_desc = match m { 562 | models::RunMode::Always => "always", 563 | models::RunMode::Auto => "auto", 564 | models::RunMode::Never => "never", 565 | models::RunMode::Restore => "restore", 566 | } 567 | .to_string(); 568 | 569 | let mut node = treexml::Element::new(format!("set_{}_mode", &comp_desc)); 570 | let mut dur_node = treexml::Element::new("duration"); 571 | dur_node.text = Some(format!("{}", duration)); 572 | node.children.push(dur_node); 573 | node.children.push(treexml::Element::new(mode_desc)); 574 | node 575 | }]) 576 | .await?; 577 | verify_rpc_reply_contents(&rsp_root)?; 578 | Ok(()) 579 | } 580 | 581 | pub async fn get_host_info(&mut self) -> Result { 582 | self.get_object_by_req_tag("get_host_info", "host_info") 583 | .await 584 | } 585 | 586 | pub async fn set_language(&mut self, v: &str) -> Result<(), Error> { 587 | self.transport.ready().await?; 588 | verify_rpc_reply_contents( 589 | &self 590 | .transport 591 | .call(vec![{ 592 | let mut node = treexml::Element::new("set_language"); 593 | let mut language_node = treexml::Element::new("language"); 594 | language_node.text = Some(v.into()); 595 | node.children.push(language_node); 596 | node 597 | }]) 598 | .await?, 599 | )?; 600 | Ok(()) 601 | } 602 | } 603 | 604 | #[cfg(test)] 605 | mod tests { 606 | use super::errors::Error; 607 | 608 | #[test] 609 | fn verify_rpc_reply_contents() { 610 | let mut fixture = treexml::Element::new("error"); 611 | fixture.text = Some("Missing authenticator".into()); 612 | let fixture = vec![fixture]; 613 | assert_eq!( 614 | super::verify_rpc_reply_contents(&fixture).err().unwrap(), 615 | Error::AuthError("Missing authenticator".to_string()) 616 | ); 617 | } 618 | } 619 | --------------------------------------------------------------------------------