summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFxQnLr <[email protected]>2023-11-17 11:18:31 +0100
committerGitHub <[email protected]>2023-11-17 11:18:31 +0100
commit48190366ac94888811dd4b0b8e6532b35f1a9d10 (patch)
tree8689660d75d0e45af460ed3d144b5a010289c0be /src
parent8fab2e7c3a38a91c8f5549b639e7f2ac4ae1a420 (diff)
parent93de8742961287cb9cfd08e68c8afa2347585a73 (diff)
downloadwebol-cli-48190366ac94888811dd4b0b8e6532b35f1a9d10.tar
webol-cli-48190366ac94888811dd4b0b8e6532b35f1a9d10.tar.gz
webol-cli-48190366ac94888811dd4b0b8e6532b35f1a9d10.zip
Merge pull request #1 from FxQnLr/eta
Eta
Diffstat (limited to 'src')
-rw-r--r--src/error.rs6
-rw-r--r--src/main.rs91
-rw-r--r--src/requests/device.rs44
-rw-r--r--src/requests/start.rs131
4 files changed, 224 insertions, 48 deletions
diff --git a/src/error.rs b/src/error.rs
index d35991b..f15c60a 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,9 +1,11 @@
1use std::fmt::Debug; 1use std::{fmt::Debug, num::ParseIntError};
2 2
3pub enum CliError { 3pub enum CliError {
4 Reqwest(reqwest::Error), 4 Reqwest(reqwest::Error),
5 Config(config::ConfigError), 5 Config(config::ConfigError),
6 Serde(serde_json::Error), 6 Serde(serde_json::Error),
7 Parse(ParseIntError),
8 WsResponse,
7} 9}
8 10
9impl Debug for CliError { 11impl Debug for CliError {
@@ -12,6 +14,8 @@ impl Debug for CliError {
12 Self::Reqwest(err) => { err.fmt(f) }, 14 Self::Reqwest(err) => { err.fmt(f) },
13 Self::Config(err) => { err.fmt(f) }, 15 Self::Config(err) => { err.fmt(f) },
14 Self::Serde(err) => { err.fmt(f) }, 16 Self::Serde(err) => { err.fmt(f) },
17 Self::Parse(err) => { err.fmt(f) },
18 Self::WsResponse => { f.write_str("Error in Response") },
15 } 19 }
16 } 20 }
17} 21}
diff --git a/src/main.rs b/src/main.rs
index ab7e476..afe6fac 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,10 @@
1use clap::{Parser, Subcommand}; 1use std::{fmt::Display, time::Duration};
2
3use clap::{Parser, Command, CommandFactory, Subcommand};
4use clap_complete::{generate, Shell, Generator};
2use config::SETTINGS; 5use config::SETTINGS;
3use error::CliError; 6use error::CliError;
7use indicatif::{ProgressBar, ProgressStyle, MultiProgress};
4use requests::{start::start, device}; 8use requests::{start::start, device};
5use reqwest::header::{HeaderMap, HeaderValue}; 9use reqwest::header::{HeaderMap, HeaderValue};
6use serde::Deserialize; 10use serde::Deserialize;
@@ -9,7 +13,15 @@ mod config;
9mod error; 13mod error;
10mod requests; 14mod requests;
11 15
12/// webol http client 16static OVERVIEW_STYLE: &str = "{spinner:.green} ({elapsed}{wide_msg}";
17static OVERVIEW_ERROR: &str = "✗ ({elapsed}) {wide_msg}";
18static OVERVIEW_DONE: &str = "✓ ({elapsed}) {wide_msg}";
19static DEFAULT_STYLE: &str = " {spinner:.green} {wide_msg}";
20static DONE_STYLE: &str = " ✓ {wide_msg}";
21static ERROR_STYLE: &str = " ✗ {wide_msg}";
22static TICK_SPEED: u64 = 1000 / 16;
23
24/// webol client
13#[derive(Parser)] 25#[derive(Parser)]
14#[command(author, version, about, long_about = None)] 26#[command(author, version, about, long_about = None)]
15struct Args { 27struct Args {
@@ -21,12 +33,17 @@ struct Args {
21enum Commands { 33enum Commands {
22 Start { 34 Start {
23 /// id of the device 35 /// id of the device
24 id: String 36 id: String,
37 #[arg(short, long)]
38 ping: Option<bool>
25 }, 39 },
26 Device { 40 Device {
27 #[command(subcommand)] 41 #[command(subcommand)]
28 devicecmd: DeviceCmd, 42 devicecmd: DeviceCmd,
29 } 43 },
44 CliGen {
45 id: Shell,
46 },
30} 47}
31 48
32#[derive(Subcommand)] 49#[derive(Subcommand)]
@@ -34,7 +51,8 @@ enum DeviceCmd {
34 Add { 51 Add {
35 id: String, 52 id: String,
36 mac: String, 53 mac: String,
37 broadcast_addr: String 54 broadcast_addr: String,
55 ip: String
38 }, 56 },
39 Get { 57 Get {
40 id: String, 58 id: String,
@@ -42,35 +60,46 @@ enum DeviceCmd {
42 Edit { 60 Edit {
43 id: String, 61 id: String,
44 mac: String, 62 mac: String,
45 broadcast_addr: String 63 broadcast_addr: String,
64 ip: String
46 }, 65 },
47} 66}
48 67
49fn main() -> Result<(), CliError> { 68#[tokio::main]
69async fn main() -> Result<(), CliError> {
50 let cli = Args::parse(); 70 let cli = Args::parse();
51 71
52 match cli.commands { 72 match cli.commands {
53 Commands::Start { id } => { 73 Commands::Start { id, ping } => {
54 start(id)?; 74 start(id, ping.unwrap_or(true)).await?;
55 }, 75 },
56 Commands::Device { devicecmd } => { 76 Commands::Device { devicecmd } => {
57 match devicecmd { 77 match devicecmd {
58 DeviceCmd::Add { id, mac, broadcast_addr } => { 78 DeviceCmd::Add { id, mac, broadcast_addr, ip } => {
59 device::put(id, mac, broadcast_addr)?; 79 device::put(id, mac, broadcast_addr, ip).await?;
60 }, 80 },
61 DeviceCmd::Get { id } => { 81 DeviceCmd::Get { id } => {
62 device::get(id)?; 82 device::get(id).await?;
63 }, 83 },
64 DeviceCmd::Edit { id, mac, broadcast_addr } => { 84 DeviceCmd::Edit { id, mac, broadcast_addr, ip } => {
65 device::post(id, mac, broadcast_addr)?; 85 device::post(id, mac, broadcast_addr, ip).await?;
66 }, 86 },
67 } 87 }
88 },
89 Commands::CliGen { id } => {
90 eprintln!("Generating completion file for {id:?}...");
91 let mut cmd = Args::command();
92 print_completions(id, &mut cmd)
68 } 93 }
69 } 94 }
70 95
71 Ok(()) 96 Ok(())
72} 97}
73 98
99fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
100 generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
101}
102
74fn default_headers() -> Result<HeaderMap, CliError> { 103fn default_headers() -> Result<HeaderMap, CliError> {
75 let mut map = HeaderMap::new(); 104 let mut map = HeaderMap::new();
76 map.append("Accept-Content", HeaderValue::from_str("application/json").unwrap()); 105 map.append("Accept-Content", HeaderValue::from_str("application/json").unwrap());
@@ -87,14 +116,44 @@ fn default_headers() -> Result<HeaderMap, CliError> {
87 Ok(map) 116 Ok(map)
88} 117}
89 118
90fn format_url(path: &str) -> Result<String, CliError> { 119fn format_url(path: &str, protocol: Protocols) -> Result<String, CliError> {
91 Ok(format!( 120 Ok(format!(
92 "{}/{}", 121 "{}://{}/{}",
122 protocol,
93 SETTINGS.get_string("server").map_err(CliError::Config)?, 123 SETTINGS.get_string("server").map_err(CliError::Config)?,
94 path 124 path
95 )) 125 ))
96} 126}
97 127
128fn add_pb(mp: &MultiProgress, template: &str, message: String) -> ProgressBar {
129 let pb = mp.add(ProgressBar::new(1));
130 pb.set_style(ProgressStyle::with_template(template).unwrap());
131 pb.enable_steady_tick(Duration::from_millis(TICK_SPEED));
132 pb.set_message(message);
133
134 pb
135}
136
137fn finish_pb(pb: ProgressBar, message: String, template: &str) {
138 pb.set_style(ProgressStyle::with_template(template).unwrap());
139 pb.finish_with_message(message);
140
141}
142
143enum Protocols {
144 Http,
145 Websocket,
146}
147
148impl Display for Protocols {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 match self {
151 Self::Http => f.write_str("http"),
152 Self::Websocket => f.write_str("ws")
153 }
154 }
155}
156
98#[derive(Debug, Deserialize)] 157#[derive(Debug, Deserialize)]
99struct ErrorResponse { 158struct ErrorResponse {
100 error: String 159 error: String
diff --git a/src/requests/device.rs b/src/requests/device.rs
index 525745a..cbc838e 100644
--- a/src/requests/device.rs
+++ b/src/requests/device.rs
@@ -1,55 +1,65 @@
1use crate::{error::CliError, default_headers, format_url}; 1use crate::{error::CliError, default_headers, format_url, Protocols};
2 2
3pub fn put(id: String, mac: String, broadcast_addr: String) -> Result<(), CliError> { 3pub async fn put(id: String, mac: String, broadcast_addr: String, ip: String) -> Result<(), CliError> {
4 let res = reqwest::blocking::Client::new() 4 let url = format_url("device", Protocols::Http)?;
5 .put(format_url("device")?) 5 println!("{}", url);
6 let res = reqwest::Client::new()
7 .put(url)
6 .headers(default_headers()?) 8 .headers(default_headers()?)
7 .body( 9 .body(
8 format!( 10 format!(
9 r#"{{"id": "{}", "mac": "{}", "broadcast_addr": "{}"}}"#, 11 r#"{{"id": "{}", "mac": "{}", "broadcast_addr": "{}", "ip": "{}"}}"#,
10 id, 12 id,
11 mac, 13 mac,
12 broadcast_addr 14 broadcast_addr,
15 ip
13 ) 16 )
14 ) 17 )
15 .send() 18 .send()
19 .await
16 .map_err(CliError::Reqwest)? 20 .map_err(CliError::Reqwest)?
17 .text(); 21 .text()
22 .await;
18 23
19 println!("{:?}", res); 24 println!("{:?}", res);
20 Ok(()) 25 Ok(())
21} 26}
22 27
23pub fn get(id: String) -> Result<(), CliError> { 28pub async fn get(id: String) -> Result<(), CliError> {
24 let res = reqwest::blocking::Client::new() 29 let res = reqwest::Client::new()
25 .get(format_url("device")?) 30 .get(format_url("device", Protocols::Http)?)
26 .headers(default_headers()?) 31 .headers(default_headers()?)
27 .body( 32 .body(
28 format!(r#"{{"id": "{}"}}"#, id) 33 format!(r#"{{"id": "{}"}}"#, id)
29 ) 34 )
30 .send() 35 .send()
36 .await
31 .map_err(CliError::Reqwest)? 37 .map_err(CliError::Reqwest)?
32 .text(); 38 .text()
39 .await;
33 40
34 println!("{:?}", res); 41 println!("{:?}", res);
35 Ok(()) 42 Ok(())
36} 43}
37 44
38pub fn post(id: String, mac: String, broadcast_addr: String) -> Result<(), CliError> { 45pub async fn post(id: String, mac: String, broadcast_addr: String, ip: String) -> Result<(), CliError> {
39 let res = reqwest::blocking::Client::new() 46 let res = reqwest::Client::new()
40 .post(format_url("device")?) 47 .post(format_url("device", Protocols::Http)?)
41 .headers(default_headers()?) 48 .headers(default_headers()?)
42 .body( 49 .body(
43 format!( 50 format!(
44 r#"{{"id": "{}", "mac": "{}", "broadcast_addr": "{}"}}"#, 51 r#"{{"id": "{}", "mac": "{}", "broadcast_addr": "{}", "ip": "{}"}}"#,
45 id, 52 id,
46 mac, 53 mac,
47 broadcast_addr 54 broadcast_addr,
55 ip
48 ) 56 )
49 ) 57 )
50 .send() 58 .send()
59 .await
51 .map_err(CliError::Reqwest)? 60 .map_err(CliError::Reqwest)?
52 .text(); 61 .text()
62 .await;
53 63
54 println!("{:?}", res); 64 println!("{:?}", res);
55 Ok(()) 65 Ok(())
diff --git a/src/requests/start.rs b/src/requests/start.rs
index 30f65b9..ca4ca44 100644
--- a/src/requests/start.rs
+++ b/src/requests/start.rs
@@ -1,49 +1,152 @@
1use futures_util::{StreamExt, SinkExt};
2use indicatif::{MultiProgress, ProgressBar};
1use reqwest::StatusCode; 3use reqwest::StatusCode;
2use serde::Deserialize; 4use serde::Deserialize;
5use tokio_tungstenite::{connect_async, tungstenite::Message};
3 6
4use crate::{config::SETTINGS, error::CliError, default_headers, ErrorResponse}; 7use crate::{error::CliError, default_headers, ErrorResponse, format_url, Protocols, OVERVIEW_STYLE, DEFAULT_STYLE, DONE_STYLE, finish_pb, ERROR_STYLE, OVERVIEW_ERROR, OVERVIEW_DONE, add_pb};
5 8
6pub fn start(id: String) -> Result<(), CliError> { 9pub async fn start(id: String, ping: bool) -> Result<(), CliError> {
7 let res = reqwest::blocking::Client::new() 10
8 .post( 11 let send_start = MultiProgress::new();
9 format!( 12 let overview = add_pb(&send_start, OVERVIEW_STYLE, format!(") start {}", id));
10 "{}/start", 13
11 SETTINGS.get_string("server").map_err(CliError::Config)? 14 // TODO: calculate average start-time on server
12 ) 15 let url = format_url("start", Protocols::Http)?;
13 ) 16 let connect = add_pb(&send_start, DEFAULT_STYLE, format!("connect to {}", url));
17 let res = reqwest::Client::new()
18 .post(url)
14 .headers(default_headers()?) 19 .headers(default_headers()?)
15 .body( 20 .body(
16 format!(r#"{{"id": "{}"}}"#, id) 21 format!(r#"{{"id": "{}", "ping": {}}}"#, id, ping)
17 ) 22 )
18 .send() 23 .send()
24 .await
19 .map_err(CliError::Reqwest)?; 25 .map_err(CliError::Reqwest)?;
26 finish_pb(connect, "connected, got response".to_string(), DONE_STYLE);
20 27
28 let res_pb = add_pb(&send_start, DEFAULT_STYLE, "analyzing response".to_string());
21 match res.status() { 29 match res.status() {
22 StatusCode::OK => { 30 StatusCode::OK => {
23 let body = serde_json::from_str::<StartResponse>( 31 let body = serde_json::from_str::<StartResponse>(
24 &res.text().map_err(CliError::Reqwest)? 32 &res.text().await.map_err(CliError::Reqwest)?
25 ) 33 )
26 .map_err(CliError::Serde)?; 34 .map_err(CliError::Serde)?;
27 35
28 if body.boot { 36 if body.boot {
29 println!("successfully started {}", body.id); 37 finish_pb(res_pb, "sent start packet".to_string(), DONE_STYLE);
38 }
39
40 if ping {
41 let status = status_socket(body.uuid, &send_start, &overview, id).await?;
42 if status {
43 finish_pb(overview, format!("successfully started {}", body.id), OVERVIEW_DONE);
44 } else {
45 finish_pb(overview, format!("error while starting {}", body.id), OVERVIEW_ERROR);
46 }
30 } 47 }
31 }, 48 },
32 _ => { 49 _ => {
33 let body = serde_json::from_str::<ErrorResponse>( 50 let body = serde_json::from_str::<ErrorResponse>(
34 &res.text().map_err(CliError::Reqwest)? 51 &res.text().await.map_err(CliError::Reqwest)?
35 ) 52 )
36 .map_err(CliError::Serde)?; 53 .map_err(CliError::Serde)?;
37 54
38 println!("got error: {}", body.error); 55 res_pb.finish_with_message(format!("got error: {}", body.error));
39 } 56 }
40 } 57 }
41 58
42 Ok(()) 59 Ok(())
43} 60}
44 61
62async fn status_socket(uuid: String, pb: &MultiProgress, overview: &ProgressBar, id: String) -> Result<bool, CliError> {
63 // TODO: Remove unwraps
64 let ws_pb = add_pb(pb, DEFAULT_STYLE, "connect to websocket".to_string());
65 let (mut ws_stream, _response) = connect_async(format_url("status", Protocols::Websocket)?)
66 .await
67 .expect("Failed to connect");
68 finish_pb(ws_pb, "connected to websocket".to_string(), DONE_STYLE);
69
70 ws_stream.send(Message::Text(uuid.clone())).await.unwrap();
71
72 // Get ETA
73 let eta_msg = ws_stream.next().await.unwrap().unwrap();
74 let eta = get_eta(eta_msg.into_text().unwrap(), uuid.clone())? + overview.elapsed().as_secs();
75 overview.set_message(format!("/{}) start {}", eta, id));
76
77 let msg_pb = add_pb(pb, DEFAULT_STYLE, "await message".to_string());
78 let msg = ws_stream.next().await.unwrap();
79 finish_pb(msg_pb, "received message".to_string(), DONE_STYLE);
80
81 ws_stream.close(None).await.unwrap();
82
83 let v_pb = add_pb(pb, DEFAULT_STYLE, "verify response".to_string());
84 let res = verify_response(msg.unwrap().to_string(), uuid)?;
85 match res {
86 Verified::WrongUuid => {
87 finish_pb(v_pb, "returned wrong uuid".to_string(), ERROR_STYLE);
88 Ok(false)
89 },
90 Verified::ResponseType(res_type) => {
91 match res_type {
92 ResponseType::Start => {
93 finish_pb(v_pb, "device started".to_string(), DONE_STYLE);
94 Ok(true)
95 },
96 ResponseType::Timeout => {
97 finish_pb(v_pb, "ping timed out".to_string(), ERROR_STYLE);
98 Ok(false)
99 },
100 ResponseType::NotFound => {
101 finish_pb(v_pb, "unknown uuid".to_string(), ERROR_STYLE);
102 Ok(false)
103 },
104 }
105 }
106 }
107}
108
109fn get_eta(msg: String, uuid: String) -> Result<u64, CliError> {
110 let spl: Vec<&str> = msg.split('_').collect();
111 if (spl[0] != "eta") || (spl[2] != uuid) { return Err(CliError::WsResponse); };
112 Ok(u64::from_str_radix(spl[1], 10).map_err(CliError::Parse)?)
113}
114
115fn verify_response(res: String, org_uuid: String) -> Result<Verified, CliError> {
116 let spl: Vec<&str> = res.split('_').collect();
117 let res_type = spl[0];
118 let uuid = spl[1];
119
120 if uuid != org_uuid { return Ok(Verified::WrongUuid) };
121
122 Ok(Verified::ResponseType(ResponseType::from(res_type)?))
123}
124
45#[derive(Debug, Deserialize)] 125#[derive(Debug, Deserialize)]
46struct StartResponse { 126struct StartResponse {
47 boot: bool, 127 boot: bool,
48 id: String, 128 id: String,
129 uuid: String,
130}
131
132enum Verified {
133 ResponseType(ResponseType),
134 WrongUuid
135}
136
137enum ResponseType {
138 Start,
139 Timeout,
140 NotFound,
141}
142
143impl ResponseType {
144 fn from(value: &str) -> Result<Self, CliError> {
145 match value {
146 "start" => Ok(ResponseType::Start),
147 "timeout" => Ok(ResponseType::Timeout),
148 "notfound" => Ok(ResponseType::NotFound),
149 _ => Err(CliError::WsResponse),
150 }
151 }
49} 152}