1use crate::config::{ClientConfig, ClientOpts};
2use crate::shell::SubProcess;
3use anyhow::{bail, Context, Result};
4use async_trait::async_trait;
5use cloudpub_common::fair_channel::FairSender;
6use cloudpub_common::protocol::message::Message;
7use cloudpub_common::protocol::{Break, ErrorInfo, ErrorKind, ServerEndpoint};
8use cloudpub_common::utils::is_tcp_port_available;
9use parking_lot::RwLock;
10use std::sync::Arc;
11use tokio::sync::mpsc;
12use tokio::task::JoinHandle;
13use tokio::time::{self, Duration, Instant};
14use tracing::{debug, error};
15
16fn encode_client_config(endpoint: &ServerEndpoint) -> Vec<u8> {
19 use cloudpub_common::prost::Message as ProstMessage;
20 endpoint
21 .client
22 .as_ref()
23 .map(|c| c.encode_to_vec())
24 .unwrap_or_default()
25}
26
27#[async_trait]
28pub trait Plugin: Send + Sync {
29 fn name(&self) -> &'static str;
31
32 async fn setup(
34 &self,
35 config: &Arc<RwLock<ClientConfig>>,
36 opts: &ClientOpts,
37 command_rx: &mut mpsc::Receiver<Message>,
38 result_tx: &mpsc::Sender<Message>,
39 ) -> Result<()>;
40
41 async fn publish(
43 &self,
44 endpoint: &ServerEndpoint,
45 config: &Arc<RwLock<ClientConfig>>,
46 opts: &ClientOpts,
47 result_tx: &mpsc::Sender<Message>,
48 ) -> Result<SubProcess>;
49}
50
51pub struct PluginHandle {
52 guid: String,
53 client_config: Vec<u8>,
56 server: Arc<RwLock<Option<SubProcess>>>,
57 cancel_tx: mpsc::Sender<Message>,
58 task: JoinHandle<()>,
59}
60
61impl PluginHandle {
62 pub fn spawn(
63 plugin: Arc<dyn Plugin>,
64 mut endpoint: ServerEndpoint,
65 config: Arc<RwLock<ClientConfig>>,
66 opts: ClientOpts,
67 to_server_tx: FairSender<Message>,
68 ) -> Self {
69 let guid = endpoint.guid.clone();
70 let client_config = encode_client_config(&endpoint);
71 let guid_for_task = guid.clone();
72 let server = Arc::new(RwLock::new(None));
73 let server_task = server.clone();
74
75 let (cancel_tx, mut cancel_rx) = mpsc::channel::<Message>(1);
77 let (proc_event_tx, mut proc_event_rx) = mpsc::channel::<Message>(1024);
78
79 let task = tokio::spawn(async move {
80 tokio::spawn({
82 let mut endpoint = endpoint.clone();
83 let to_server_tx = to_server_tx.clone();
84 let guid_for_task = guid_for_task.clone();
85 async move {
86 use tokio::time::{Duration, Instant};
87
88 let mut last_progress_time = Instant::now() - Duration::from_secs(1); while let Some(mut msg) = proc_event_rx.recv().await {
91 let should_send = match &mut msg {
92 Message::Progress(progress_info) => {
93 progress_info.guid = guid_for_task.clone();
94
95 let now = Instant::now();
96 if progress_info.current == 0
98 || progress_info.current >= progress_info.total
99 || now.duration_since(last_progress_time)
100 >= Duration::from_secs(1)
101 {
102 last_progress_time = now;
103 true
104 } else {
105 false
106 }
107 }
108 Message::Error(info) => {
109 endpoint.error = info.message.clone();
113 true
114 }
115 _ => true, };
117
118 if should_send {
119 to_server_tx.send(msg.clone()).await.ok();
120 }
121 }
122
123 endpoint.status = Some("offline".to_string());
124 to_server_tx
125 .send(Message::EndpointStatus(endpoint.clone()))
126 .await
127 .ok();
128 }
129 });
130
131 endpoint.status = Some("waiting".to_string());
133 let _ = to_server_tx
134 .send(Message::EndpointStatus(endpoint.clone()))
135 .await;
136
137 let res: Result<()> = async {
138 plugin
140 .setup(&config, &opts, &mut cancel_rx, &proc_event_tx)
141 .await
142 .context("Failed to setup plugin")?;
143
144 let server_process = plugin
145 .publish(&endpoint, &config, &opts, &proc_event_tx)
146 .await
147 .context("Failed to publish plugin service")?;
148
149 let now = Instant::now();
151 while is_tcp_port_available("127.0.0.1", server_process.port)
152 .await
153 .context("Check port availability")?
154 {
155 if server_process.result.read().is_err() {
156 return Ok(()); }
158
159 if now.elapsed() > Duration::from_secs(60) {
160 bail!("{}", crate::t!("error-start-server"));
161 }
162 debug!(
163 "Waiting for server to start on port {}",
164 server_process.port
165 );
166 time::sleep(Duration::from_secs(1)).await;
167 }
168
169 *server_task.write() = Some(server_process);
170 Ok(())
171 }
172 .await;
173
174 match res {
175 Ok(()) => {
176 endpoint.status = Some("online".into());
177 let _ = to_server_tx.send(Message::EndpointStatus(endpoint)).await;
178 }
179 Err(e) => {
180 error!("Error handling endpoint {}: {:#}", &guid_for_task, e);
181 let _ = proc_event_tx
187 .send(Message::Error(ErrorInfo {
188 kind: ErrorKind::PublishFailed.into(),
189 message: format!("{:#}", e),
190 guid: guid_for_task,
191 }))
192 .await;
193 }
194 }
195 });
196
197 Self {
198 guid,
199 client_config,
200 server,
201 cancel_tx,
202 task,
203 }
204 }
205
206 pub fn same_config(&self, endpoint: &ServerEndpoint) -> bool {
211 self.client_config == encode_client_config(endpoint)
212 }
213
214 pub fn is_healthy(&self) -> bool {
218 if !self.task.is_finished() {
219 return true;
220 }
221 match self.server.read().as_ref() {
222 Some(process) => process.result.read().is_ok(),
223 None => false,
224 }
225 }
226
227 pub fn port(&self) -> Option<u16> {
228 self.server.read().as_ref().map(|s| s.port)
229 }
230
231 pub fn guid(&self) -> &str {
232 &self.guid
233 }
234
235 pub fn send_break(&self) {
236 let _ = self.cancel_tx.try_send(Message::Break(Break {
237 guid: self.guid.clone(),
238 }));
239 }
240}
241
242impl Drop for PluginHandle {
243 fn drop(&mut self) {
244 self.task.abort();
246 let _ = self.cancel_tx.try_send(Message::Break(Break {
248 guid: self.guid.clone(),
249 }));
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use anyhow::anyhow;
257 use cloudpub_common::fair_channel::fair_channel;
258 use tokio::time::timeout;
259
260 struct SetupFails;
277
278 #[async_trait]
279 impl Plugin for SetupFails {
280 fn name(&self) -> &'static str {
281 "setup_fails"
282 }
283
284 async fn setup(
285 &self,
286 _config: &Arc<RwLock<ClientConfig>>,
287 _opts: &ClientOpts,
288 _command_rx: &mut mpsc::Receiver<Message>,
289 _result_tx: &mpsc::Sender<Message>,
290 ) -> Result<()> {
291 Err(anyhow!("HTTP request failed with status: 404 Not Found")
294 .context("web server package download failed"))
295 }
296
297 async fn publish(
298 &self,
299 _endpoint: &ServerEndpoint,
300 _config: &Arc<RwLock<ClientConfig>>,
301 _opts: &ClientOpts,
302 _result_tx: &mpsc::Sender<Message>,
303 ) -> Result<SubProcess> {
304 unreachable!("setup failed before publish was reached")
305 }
306 }
307
308 #[tokio::test]
309 async fn failed_plugin_setup_reports_reason_in_offline_status() {
310 let (to_server_tx, mut to_server_rx) = fair_channel::<Message>(16);
311 let endpoint = ServerEndpoint {
312 guid: "guid-1505".to_string(),
313 ..Default::default()
314 };
315
316 let handle = PluginHandle::spawn(
317 Arc::new(SetupFails),
318 endpoint,
319 Arc::new(RwLock::new(ClientConfig::default())),
320 ClientOpts::default(),
321 to_server_tx,
322 );
323
324 let mut error_message: Option<String> = None;
325 let mut last_status: Option<(Option<String>, String)> = None;
326 loop {
329 match timeout(Duration::from_secs(1), to_server_rx.recv()).await {
330 Ok(Some(msg)) => match msg {
331 Message::Error(info) => error_message = Some(info.message),
332 Message::EndpointStatus(ep) => {
333 last_status = Some((ep.status.clone(), ep.error.clone()))
334 }
335 other => debug!("unexpected message: {:?}", other),
336 },
337 Ok(None) => break,
338 Err(_) => break,
339 }
340 }
341
342 let error_message = error_message.expect("no Message::Error reached the server channel");
343 assert!(
344 error_message.contains("Failed to setup plugin")
345 && error_message.contains("web server package download failed")
346 && error_message.contains("404 Not Found"),
347 "Error message must carry the full chain, got: {}",
348 error_message
349 );
350
351 let (status, status_error) =
352 last_status.expect("no EndpointStatus reached the server channel");
353 assert_eq!(
354 status.as_deref(),
355 Some("offline"),
356 "expected the final offline status, got status={:?} error={}",
357 status,
358 status_error
359 );
360 assert!(
361 status_error.contains("404 Not Found"),
362 "the offline status must carry the failure reason, got: {}",
363 status_error
364 );
365
366 drop(handle);
367 }
368}