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 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 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 #[must_use]
154 pub fn socket_path(&self) -> PathBuf {
155 self.system.join(SOCKET_FILE)
156 }
157
158 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 #[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}