diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/error.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 91 | ||||
-rw-r--r-- | src/requests/device.rs | 44 | ||||
-rw-r--r-- | src/requests/start.rs | 131 |
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 @@ | |||
1 | use std::fmt::Debug; | 1 | use std::{fmt::Debug, num::ParseIntError}; |
2 | 2 | ||
3 | pub enum CliError { | 3 | pub 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 | ||
9 | impl Debug for CliError { | 11 | impl 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 @@ | |||
1 | use clap::{Parser, Subcommand}; | 1 | use std::{fmt::Display, time::Duration}; |
2 | |||
3 | use clap::{Parser, Command, CommandFactory, Subcommand}; | ||
4 | use clap_complete::{generate, Shell, Generator}; | ||
2 | use config::SETTINGS; | 5 | use config::SETTINGS; |
3 | use error::CliError; | 6 | use error::CliError; |
7 | use indicatif::{ProgressBar, ProgressStyle, MultiProgress}; | ||
4 | use requests::{start::start, device}; | 8 | use requests::{start::start, device}; |
5 | use reqwest::header::{HeaderMap, HeaderValue}; | 9 | use reqwest::header::{HeaderMap, HeaderValue}; |
6 | use serde::Deserialize; | 10 | use serde::Deserialize; |
@@ -9,7 +13,15 @@ mod config; | |||
9 | mod error; | 13 | mod error; |
10 | mod requests; | 14 | mod requests; |
11 | 15 | ||
12 | /// webol http client | 16 | static OVERVIEW_STYLE: &str = "{spinner:.green} ({elapsed}{wide_msg}"; |
17 | static OVERVIEW_ERROR: &str = "✗ ({elapsed}) {wide_msg}"; | ||
18 | static OVERVIEW_DONE: &str = "✓ ({elapsed}) {wide_msg}"; | ||
19 | static DEFAULT_STYLE: &str = " {spinner:.green} {wide_msg}"; | ||
20 | static DONE_STYLE: &str = " ✓ {wide_msg}"; | ||
21 | static ERROR_STYLE: &str = " ✗ {wide_msg}"; | ||
22 | static 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)] |
15 | struct Args { | 27 | struct Args { |
@@ -21,12 +33,17 @@ struct Args { | |||
21 | enum Commands { | 33 | enum 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 | ||
49 | fn main() -> Result<(), CliError> { | 68 | #[tokio::main] |
69 | async 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 | ||
99 | fn print_completions<G: Generator>(gen: G, cmd: &mut Command) { | ||
100 | generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout()); | ||
101 | } | ||
102 | |||
74 | fn default_headers() -> Result<HeaderMap, CliError> { | 103 | fn 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 | ||
90 | fn format_url(path: &str) -> Result<String, CliError> { | 119 | fn 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 | ||
128 | fn 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 | |||
137 | fn 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 | |||
143 | enum Protocols { | ||
144 | Http, | ||
145 | Websocket, | ||
146 | } | ||
147 | |||
148 | impl 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)] |
99 | struct ErrorResponse { | 158 | struct 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 @@ | |||
1 | use crate::{error::CliError, default_headers, format_url}; | 1 | use crate::{error::CliError, default_headers, format_url, Protocols}; |
2 | 2 | ||
3 | pub fn put(id: String, mac: String, broadcast_addr: String) -> Result<(), CliError> { | 3 | pub 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 | ||
23 | pub fn get(id: String) -> Result<(), CliError> { | 28 | pub 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 | ||
38 | pub fn post(id: String, mac: String, broadcast_addr: String) -> Result<(), CliError> { | 45 | pub 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 @@ | |||
1 | use futures_util::{StreamExt, SinkExt}; | ||
2 | use indicatif::{MultiProgress, ProgressBar}; | ||
1 | use reqwest::StatusCode; | 3 | use reqwest::StatusCode; |
2 | use serde::Deserialize; | 4 | use serde::Deserialize; |
5 | use tokio_tungstenite::{connect_async, tungstenite::Message}; | ||
3 | 6 | ||
4 | use crate::{config::SETTINGS, error::CliError, default_headers, ErrorResponse}; | 7 | use 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 | ||
6 | pub fn start(id: String) -> Result<(), CliError> { | 9 | pub 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 | ||
62 | async 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 | |||
109 | fn 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 | |||
115 | fn 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)] |
46 | struct StartResponse { | 126 | struct StartResponse { |
47 | boot: bool, | 127 | boot: bool, |
48 | id: String, | 128 | id: String, |
129 | uuid: String, | ||
130 | } | ||
131 | |||
132 | enum Verified { | ||
133 | ResponseType(ResponseType), | ||
134 | WrongUuid | ||
135 | } | ||
136 | |||
137 | enum ResponseType { | ||
138 | Start, | ||
139 | Timeout, | ||
140 | NotFound, | ||
141 | } | ||
142 | |||
143 | impl 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 | } |