Skip to main content

core_lib/daemon/
server.rs

1#![allow(dead_code)]
2use std::sync::Arc;
3
4use crate::{
5    config::ProjectConfig,
6    core::{
7        id::short_id,
8        manager::get_watch_ctx,
9        state::{AppState, get_id_by_name, get_name_by_id},
10        watcher::{WatchContext, WatchContextBuilder},
11    },
12    daemon::utils::extract_repo_path,
13    exec::{metrics::ExecMetrics, pipeline::run_pipeline},
14    git::repo::Repo,
15    log::logger::Logger,
16};
17use anyhow::Result;
18use serde::{Deserialize, Serialize};
19use tokio::{
20    fs::{File, OpenOptions},
21    io::{AsyncWriteExt, WriteHalf},
22    net::UnixStream,
23};
24
25#[derive(Serialize, Deserialize, Debug, PartialEq)]
26#[serde(tag = "action")]
27pub enum DaemonRequest {
28    #[serde(rename = "add_watch")]
29    AddWatch {
30        project_dir: String,
31        // use Box (clippy)
32        repo: Box<Repo>,
33        config: Box<ProjectConfig>,
34    },
35
36    #[serde(rename = "run_pipeline")]
37    RunPipeline {
38        id: String,
39    },
40
41    #[serde(rename = "stop_watch")]
42    StopWatch {
43        id: String,
44    },
45
46    #[serde(rename = "up_watch")]
47    UpWatch {
48        id: String,
49    },
50
51    #[serde(rename = "rm_watch")]
52    RmWatch {
53        id: String,
54    },
55
56    #[serde(rename = "list_watch")]
57    ListWatches {
58        all: bool,
59    },
60
61    #[serde(rename = "logs_watch")]
62    LogsWatches {
63        id: String,
64        f: bool,
65    },
66
67    None,
68}
69
70#[derive(Serialize, Deserialize, Debug, PartialEq)]
71pub struct WatchInfo {
72    pub branch: String,
73    pub project_dir: String,
74    pub short_commit: String,
75    pub short_url: String,
76    pub repo_name: String,
77    pub id: String,
78    pub paused: bool,
79}
80
81#[derive(Serialize, Deserialize, Debug, PartialEq)]
82pub enum DaemonResponse {
83    Success(String),
84    Error(String),
85    ListWatches(Vec<WatchInfo>),
86    LogWatch(String, bool),
87    Ignore,
88    None,
89}
90
91pub async fn get_log_file(ctx: &WatchContext) -> Result<File> {
92    let log_path = ctx.log_path();
93    let log_file = OpenOptions::new()
94        .create(true)
95        .append(true)
96        .open(&log_path)
97        .await?;
98    Ok(log_file)
99}
100
101async fn get_logs_by_id(id: &str) -> Result<String> {
102    let log_path = WatchContext::log_path_by_id(id);
103
104    match log_path.to_str() {
105        Some(p) => Ok(String::from(p)),
106        None => Err(anyhow::anyhow!("Failed to find log path")),
107    }
108}
109
110/// Handles a daemon request and sends the resulting [`DaemonResponse`] back to the client.
111/// All errors inside handlers are mapped to `DaemonResponse::Error`.
112pub async fn handle_request(
113    req: DaemonRequest,
114    state: Arc<AppState>,
115    stream: &mut WriteHalf<UnixStream>,
116) -> Result<(), anyhow::Error> {
117    let response = match req {
118        DaemonRequest::AddWatch {
119            project_dir,
120            repo,
121            config,
122        } => handle_add_watch(state, project_dir, *repo, *config).await?,
123
124        DaemonRequest::StopWatch { id } => handle_stop_watch(state, id).await,
125
126        DaemonRequest::UpWatch { id } => handle_up_watch(state, id).await,
127
128        DaemonRequest::RmWatch { id } => handle_rm_watch(state, id).await,
129
130        DaemonRequest::ListWatches { all } => handle_list_watches(state, all).await,
131
132        DaemonRequest::LogsWatches { id, f } => handle_logs_watches(id, f).await,
133
134        DaemonRequest::RunPipeline { id } => {
135            handle_run_pipeline(&id, state, stream).await?;
136            DaemonResponse::Ignore
137        }
138        DaemonRequest::None => DaemonResponse::None,
139    };
140
141    if response != DaemonResponse::Ignore {
142        send_response(stream, response).await?;
143    }
144    Ok(())
145}
146
147async fn handle_run_pipeline(
148    id: &str,
149    state: Arc<AppState>,
150    stream: &mut WriteHalf<UnixStream>,
151) -> anyhow::Result<()> {
152    if let Some(ctx) = get_watch_ctx(&state, id).await {
153        send_response(
154            stream,
155            DaemonResponse::Success(format!("Pipeline {id} has been runed")),
156        )
157        .await?;
158        match run_pipeline(Arc::new(ctx)).await {
159            Ok(_) => {
160                tracing::info!(
161                    target: "fleet",
162                    log_path = %crate::log::logger::Logger::path_by_id(id).display(),
163                    message = "Update succeeded",
164                );
165            }
166            Err(e) => {
167                tracing::error!(
168                    target: "fleet",
169                    log_path = %crate::log::logger::Logger::path_by_id(id).display(),
170                    message = %format!("Update failed => {e}"),
171                );
172            }
173        }
174    }
175    Ok(())
176}
177
178/// Registers a new watch, updates the application state, and returns a response.
179/// Any error will be converted to `DaemonResponse::Error`.
180async fn handle_add_watch(
181    state: Arc<AppState>,
182    project_dir: String,
183    repo: Repo,
184    config: ProjectConfig,
185) -> anyhow::Result<DaemonResponse> {
186    let id = {
187        let guard = state.watches.read().await;
188        guard
189            .iter()
190            .find(|(_, ctx)| ctx.project_dir == project_dir)
191            .map(|(id, _ctx)| id.clone())
192            .unwrap_or_else(short_id)
193    };
194
195    let ctx = WatchContextBuilder::new(repo, config, project_dir, id.clone())
196        .build()
197        .await?;
198
199    let result = async {
200        // Disk I/O happens outside the state lock.
201        AppState::add_watch(&ctx).await?;
202        {
203            let mut guard = state.watches.write().await;
204            guard.retain(|_, existing_ctx| existing_ctx.project_dir != ctx.project_dir);
205            guard.insert(id.clone(), ctx.clone());
206        }
207        tracing::info!(
208            target: "fleet",
209            log_path = %ctx.log_path().display(),
210            message = %format!("Project registered with ID : {id}"),
211        );
212        Ok::<_, anyhow::Error>(())
213    }
214    .await;
215
216    match result {
217        Ok(_) => Ok(DaemonResponse::Success(format!(
218            "📌 Project registered with ID: {id}"
219        ))),
220        Err(e) => Ok(DaemonResponse::Error(format!("Failed to add watch: {e}"))),
221    }
222}
223
224/// Stops a watch by ID if it exists in the application state.
225pub async fn handle_stop_watch(state: Arc<AppState>, id: String) -> DaemonResponse {
226    match async {
227        let updated = {
228            let mut guard = state.watches.write().await;
229            match guard.get_mut(&id) {
230                Some(w) => {
231                    w.stop();
232                    Some(w.clone())
233                }
234                None => None,
235            }
236        };
237        match updated {
238            Some(w) => {
239                AppState::add_watch(&w).await?;
240                Ok::<_, anyhow::Error>(format!("🛑 Watch stopped for ID: {id}"))
241            }
242            None => Err(anyhow::anyhow!("âš  ID not found: {}", id)),
243        }
244    }
245    .await
246    {
247        Ok(msg) => DaemonResponse::Success(msg),
248        Err(e) => DaemonResponse::Error(format!("Failed to stop watch: {e}")),
249    }
250}
251
252/// Run a watch by ID if it exists in the application state.
253pub async fn handle_up_watch(state: Arc<AppState>, id: String) -> DaemonResponse {
254    match async {
255        let updated = {
256            let mut guard = state.watches.write().await;
257            match guard.get_mut(&id) {
258                Some(w) => {
259                    w.run();
260                    Some(w.clone())
261                }
262                None => None,
263            }
264        };
265        match updated {
266            Some(w) => {
267                AppState::add_watch(&w).await?;
268                Ok::<_, anyhow::Error>(format!("🟢 Watch up for ID: {id}"))
269            }
270            None => Err(anyhow::anyhow!("âš  ID not found: {}", id)),
271        }
272    }
273    .await
274    {
275        Ok(msg) => DaemonResponse::Success(msg),
276        Err(e) => DaemonResponse::Error(format!("Failed to stop watch: {e}")),
277    }
278}
279
280/// Rm a watch by ID if it exists in the application state.
281pub async fn handle_rm_watch(state: Arc<AppState>, id: String) -> DaemonResponse {
282    match async {
283        let removed = {
284            let mut guard = state.watches.write().await;
285            guard.remove(&id)
286        };
287        if let Some(w) = removed {
288            // All disk I/O happens after the state lock is released.
289            ExecMetrics::rm_metrics_by_id(&id)?; // remove metrics file
290            Logger::rm_logs_by_id(&id)?; // remove log file
291            AppState::remove_watch_by_id(&id).await?; // remove this watch in watches.json
292            Ok::<_, anyhow::Error>(format!("Project: {} was deleted", w.repo.name))
293        } else {
294            Err(anyhow::anyhow!("âš  ID not found: {}", id))
295        }
296    }
297    .await
298    {
299        Ok(msg) => DaemonResponse::Success(msg),
300        Err(e) => DaemonResponse::Error(format!("Failed to stop watch: {e}")),
301    }
302}
303
304/// Returns a list of all current watches as a [`DaemonResponse::ListWatches`].
305pub async fn handle_list_watches(state: Arc<AppState>, all: bool) -> DaemonResponse {
306    match async {
307        let guard = state.watches.read().await;
308        let result: Result<Vec<WatchInfo>, anyhow::Error> = guard
309            .iter()
310            .filter(|(_, ctx)| all || !ctx.paused) // if all = true everything pass, else only if paused is false they can pass
311            .map(|(id, ctx)| {
312                let short_commit = ctx
313                    .repo
314                    .branches
315                    .last_commit
316                    .chars()
317                    .take(8)
318                    .collect::<String>();
319                let short_url = extract_repo_path(&ctx.repo.remote)?;
320                let short_branch = if ctx.repo.branches.name.len() > 12 {
321                    format!("{}...", &ctx.repo.branches.name[..9])
322                } else {
323                    ctx.repo.branches.name.clone()
324                };
325                Ok(WatchInfo {
326                    branch: short_branch,
327                    project_dir: ctx.project_dir.clone(),
328                    short_commit,
329                    short_url,
330                    repo_name: ctx.repo.name.clone(),
331                    id: id.clone(),
332                    paused: ctx.paused,
333                })
334            })
335            .collect();
336        Ok::<_, anyhow::Error>(DaemonResponse::ListWatches(result?))
337    }
338    .await
339    {
340        Ok(resp) => resp,
341        Err(e) => DaemonResponse::Error(format!("Failed to list watches: {e}")),
342    }
343}
344
345/// Fetches logs for a given watch by ID or name.
346/// If the watch is not found, sends an error directly to the client.
347/// Returns `None` if an error was already sent to the stream.
348async fn handle_logs_watches(id: String, follow: bool) -> DaemonResponse {
349    match async {
350        let id = match get_name_by_id(&id).await {
351            Ok(Some(_)) => id,
352            Err(_) | Ok(None) => match get_id_by_name(&id).await? {
353                Some(uuid) => uuid,
354                None => anyhow::bail!("No repo with this name exists"),
355            },
356        };
357        let logs = get_logs_by_id(&id).await?;
358        Ok::<_, anyhow::Error>(DaemonResponse::LogWatch(logs, follow))
359    }
360    .await
361    {
362        Ok(resp) => resp,
363        Err(e) => DaemonResponse::Error(format!("Failed to fetch logs: {e}")),
364    }
365}
366
367async fn send_response(
368    stream: &mut WriteHalf<UnixStream>,
369    response: DaemonResponse,
370) -> Result<(), anyhow::Error> {
371    let response_str = serde_json::to_string(&response)? + "\n";
372    stream.write_all(response_str.as_bytes()).await?;
373    stream.flush().await?;
374    Ok(())
375}
376
377async fn send_error_response(
378    stream: &mut WriteHalf<UnixStream>,
379    message: &str,
380) -> Result<(), anyhow::Error> {
381    let response = DaemonResponse::Error(message.to_string());
382    let response_str = serde_json::to_string(&response)? + "\n";
383    stream.write_all(response_str.as_bytes()).await?;
384    stream.flush().await?;
385    Ok(())
386}