Skip to main content

tutti_daemon/
lib.rs

1use std::{path::PathBuf, process, sync::Arc};
2
3use futures_util::FutureExt;
4use tokio::sync::{mpsc::Receiver, Mutex};
5use tutti_core::{Supervisor, SupervisorEvent, UnixProcessManager};
6use tutti_transport::{
7    api::TuttiApi,
8    error::{TransportError, TransportResult},
9    server::ipc_server::IpcServer,
10};
11
12pub const SOCKET_FILE: &str = "tutti.sock";
13
14#[derive(Debug, Clone)]
15struct Context {
16    supervisor: Arc<Mutex<Supervisor>>,
17    receiver: Arc<Mutex<Receiver<SupervisorEvent>>>,
18}
19
20impl Context {
21    pub fn new(
22        supervisor: Arc<Mutex<Supervisor>>,
23        receiver: Arc<Mutex<Receiver<SupervisorEvent>>>,
24    ) -> Self {
25        Context {
26            supervisor,
27            receiver,
28        }
29    }
30}
31
32async fn unary_handler(message: TuttiApi, context: Context) -> TransportResult<TuttiApi> {
33    match message {
34        TuttiApi::Ping => Ok(TuttiApi::Pong),
35        TuttiApi::Up { project, services } => {
36            tracing::info!("Starting project {project:?} with services {services:?}");
37
38            let mut guard = context.supervisor.lock().await;
39            guard
40                .up(project, services)
41                .await
42                .map_err(|_| TransportError::UnknownMessage)?;
43
44            Ok(TuttiApi::Pong)
45        }
46        TuttiApi::Down { project_id } => {
47            tracing::info!("Stopping project {project_id:?}");
48
49            let mut guard = context.supervisor.lock().await;
50            guard
51                .down(project_id)
52                .await
53                .map_err(|_| TransportError::UnknownMessage)?;
54
55            Ok(TuttiApi::Pong)
56        }
57        TuttiApi::Shutdown => {
58            tracing::info!("Stopping supervisor");
59
60            let mut guard = context.supervisor.lock().await;
61            guard
62                .shutdown()
63                .await
64                .map_err(|_| TransportError::UnknownMessage)?;
65
66            #[allow(unsafe_code)]
67            unsafe {
68                let pid = libc::pid_t::try_from(process::id()).unwrap_or_default();
69                libc::kill(pid, libc::SIGTERM);
70            }
71
72            Ok(TuttiApi::Shutdown)
73        }
74        _ => Err(TransportError::UnknownMessage),
75    }
76}
77
78async fn stream_handler(context: Context) -> TransportResult<TuttiApi> {
79    tracing::info!("Starting stream handler");
80
81    let mut guard = context.receiver.lock().await;
82    let Some(event) = guard.recv().await else {
83        return Err(TransportError::UnknownMessage);
84    };
85
86    tracing::info!("Received event: {:?}", event);
87
88    match event {
89        SupervisorEvent::Log {
90            project_id,
91            service,
92            message,
93        } => Ok(TuttiApi::Log {
94            project_id,
95            service,
96            message,
97        }),
98        SupervisorEvent::ProjectStopped { project_id } => {
99            Ok(TuttiApi::ProjectStopped { project_id })
100        }
101        SupervisorEvent::Error {
102            project_id,
103            message,
104        } => Ok(TuttiApi::Error {
105            project_id,
106            message,
107        }),
108    }
109}
110
111#[derive(Debug)]
112pub struct DaemonRunner {
113    system: PathBuf,
114}
115
116impl DaemonRunner {
117    #[must_use]
118    pub fn new(system: PathBuf) -> Self {
119        DaemonRunner { system }
120    }
121
122    /// Prepare the system directory.
123    ///
124    /// # Errors
125    /// Returns an error if the system directory cannot be prepared.
126    pub fn prepare(&self) -> Result<(), String> {
127        if !std::fs::exists(&self.system)
128            .map_err(|err| format!("Cannot prepare system directory: {err:?}"))?
129        {
130            std::fs::create_dir_all(&self.system)
131                .map_err(|err| format!("Cannot create system directory: {err:?}"))?;
132        }
133
134        Ok(())
135    }
136
137    /// Clear the system directory.
138    ///
139    /// # Errors
140    /// Returns an error if the system directory cannot be cleared.
141    pub fn clear(&self) -> Result<(), String> {
142        if std::fs::exists(&self.system)
143            .map_err(|err| format!("Cannot clear system directory: {err:?}"))?
144        {
145            std::fs::remove_dir_all(&self.system)
146                .map_err(|err| format!("Cannot remove system directory: {err:?}"))?;
147        }
148
149        Ok(())
150    }
151
152    /// Get the socket path.
153    #[must_use]
154    pub fn socket_path(&self) -> PathBuf {
155        self.system.join(SOCKET_FILE)
156    }
157
158    /// Spawn the daemon process.
159    ///
160    /// # Errors
161    /// Returns an error if the daemon process cannot be spawned.
162    pub fn spawn(&self) -> Result<(), String> {
163        std::process::Command::new("tutti-cli")
164            .arg("daemon")
165            .arg("run")
166            .env("RUST_LOG", "ERROR")
167            .spawn()
168            .map_err(|err| format!("Cannot spawn daemon process: {err:?}"))?;
169
170        for _ in 0..10 {
171            std::thread::sleep(std::time::Duration::from_millis(100));
172            if self.socket_path().exists() {
173                return Ok(());
174            }
175        }
176
177        Err("Timeout waiting for daemon process to start".to_string())
178    }
179
180    /// Start the daemon process.
181    ///
182    /// # Errors
183    /// Returns an error if the daemon process cannot be started.
184    #[tracing::instrument(skip_all)]
185    pub async fn start(&self) -> Result<(), String> {
186        tracing::info!("Starting daemon process...");
187        let (supervisor, receiver) = Supervisor::new(UnixProcessManager::new());
188        tracing::debug!("Supervisor created");
189
190        let unary_handler =
191            Arc::new(|api: TuttiApi, context: Context| unary_handler(api, context).boxed());
192        let stream_handler = Arc::new(|context: Context| stream_handler(context).boxed());
193
194        IpcServer::<Context>::new(
195            self.system.join(SOCKET_FILE),
196            Context::new(
197                Arc::new(Mutex::new(supervisor)),
198                Arc::new(Mutex::new(receiver)),
199            ),
200        )
201        .map_err(|err| format!("Cannot start IPC Server: {err:?}"))?
202        .add_unary_handler(unary_handler)
203        .add_stream_handler(stream_handler)
204        .start()
205        .await;
206
207        Ok(())
208    }
209}