1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5use tokio::net::{UnixListener, UnixStream};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum IpcMessage {
9 ProjectEntered {
11 path: PathBuf,
12 context: String,
13 },
14 ProjectLeft {
15 path: PathBuf,
16 },
17
18 StartSession {
20 project_path: Option<PathBuf>,
21 context: String,
22 },
23 StopSession,
24 PauseSession,
25 ResumeSession,
26
27 GetStatus,
29 GetActiveSession,
30 GetProject(i64),
31 GetDailyStats(chrono::NaiveDate),
32 GetWeeklyStats,
33 GetSessionsForDate(chrono::NaiveDate),
34 GetSessionMetrics(i64),
35 GetRecentProjects,
36
37 SubscribeToUpdates,
39 UnsubscribeFromUpdates,
40 ActivityHeartbeat,
41
42 SwitchProject(i64),
44 ListProjects,
45
46 Ping,
48 Shutdown,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub enum IpcResponse {
53 Ok,
54 Success,
55 Error(String),
56 Status {
57 daemon_running: bool,
58 active_session: Option<SessionInfo>,
59 uptime: u64,
60 },
61 ActiveSession(Option<crate::models::Session>),
62 Project(Option<crate::models::Project>),
63 ProjectList(Vec<crate::models::Project>),
64 SessionList(Vec<crate::models::Session>),
65 RecentProjects(Vec<ProjectWithStats>),
66 DailyStats {
67 sessions_count: i64,
68 total_seconds: i64,
69 avg_seconds: i64,
70 },
71 WeeklyStats {
72 total_seconds: i64,
73 },
74 SessionMetrics(SessionMetrics),
75 SessionInfo(SessionInfo),
76 SubscriptionConfirmed,
77 ActivityUpdate(ActivityUpdate),
78 Pong,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ProjectWithStats {
83 pub project: crate::models::Project,
84 pub today_seconds: i64,
85 pub total_seconds: i64,
86 pub last_active: Option<chrono::DateTime<chrono::Utc>>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct SessionInfo {
91 pub id: i64,
92 pub project_name: String,
93 pub project_path: PathBuf,
94 pub start_time: chrono::DateTime<chrono::Utc>,
95 pub context: String,
96 pub duration: i64, }
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SessionMetrics {
101 pub session_id: i64,
102 pub active_duration: i64, pub total_duration: i64, pub paused_duration: i64, pub activity_score: f64, pub last_activity: chrono::DateTime<chrono::Utc>,
107 pub productivity_rating: Option<u8>, }
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ActivityUpdate {
112 pub session_id: i64,
113 pub timestamp: chrono::DateTime<chrono::Utc>,
114 pub event_type: ActivityEventType,
115 pub duration_delta: i64, }
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub enum ActivityEventType {
120 SessionStarted,
121 SessionPaused,
122 SessionResumed,
123 SessionEnded,
124 ActivityDetected,
125 IdleDetected,
126 MilestoneReached { milestone: String },
127}
128
129pub struct IpcServer {
130 listener: UnixListener,
131}
132
133impl IpcServer {
134 pub fn new(socket_path: &PathBuf) -> Result<Self> {
135 if socket_path.exists() {
137 std::fs::remove_file(socket_path)?;
138 }
139
140 if let Some(parent) = socket_path.parent() {
142 std::fs::create_dir_all(parent)?;
143 }
144
145 let listener = UnixListener::bind(socket_path)?;
146
147 #[cfg(unix)]
149 {
150 use std::os::unix::fs::PermissionsExt;
151 let perms = std::fs::Permissions::from_mode(0o600);
152 std::fs::set_permissions(socket_path, perms)?;
153 }
154
155 Ok(Self { listener })
156 }
157
158 pub async fn accept(&self) -> Result<(UnixStream, tokio::net::unix::SocketAddr)> {
159 Ok(self.listener.accept().await?)
160 }
161}
162
163pub struct IpcClient {
164 pub stream: Option<UnixStream>,
165}
166
167impl IpcClient {
168 pub async fn connect(socket_path: &PathBuf) -> Result<Self> {
169 let stream = UnixStream::connect(socket_path).await?;
170 Ok(Self {
171 stream: Some(stream),
172 })
173 }
174
175 pub fn new() -> Result<Self> {
176 Ok(Self { stream: None })
177 }
178
179 pub async fn send_message(&mut self, message: &IpcMessage) -> Result<IpcResponse> {
180 let stream = self
181 .stream
182 .as_mut()
183 .ok_or_else(|| anyhow::anyhow!("No connection established"))?;
184
185 let serialized = serde_json::to_vec(message)?;
187 let len = serialized.len() as u32;
188
189 stream.write_u32(len).await?;
191 stream.write_all(&serialized).await?;
192
193 let response_len = stream.read_u32().await?;
195 let mut response_bytes = vec![0; response_len as usize];
196 stream.read_exact(&mut response_bytes).await?;
197
198 let response: IpcResponse = serde_json::from_slice(&response_bytes)?;
200 Ok(response)
201 }
202}
203
204pub async fn read_ipc_message(stream: &mut UnixStream) -> Result<IpcMessage> {
205 let len = stream.read_u32().await?;
206 let mut buffer = vec![0; len as usize];
207 stream.read_exact(&mut buffer).await?;
208
209 let message: IpcMessage = serde_json::from_slice(&buffer)?;
210 Ok(message)
211}
212
213pub async fn write_ipc_response(stream: &mut UnixStream, response: &IpcResponse) -> Result<()> {
214 let serialized = serde_json::to_vec(response)?;
215 let len = serialized.len() as u32;
216
217 stream.write_u32(len).await?;
218 stream.write_all(&serialized).await?;
219
220 Ok(())
221}
222
223pub fn get_socket_path() -> Result<PathBuf> {
224 let data_dir = crate::utils::paths::get_data_dir()?;
225 Ok(data_dir.join("daemon.sock"))
226}
227
228pub fn get_pid_file_path() -> Result<PathBuf> {
229 let data_dir = crate::utils::paths::get_data_dir()?;
230 Ok(data_dir.join("daemon.pid"))
231}
232
233pub fn write_pid_file() -> Result<()> {
234 let pid_path = get_pid_file_path()?;
235 let pid = std::process::id();
236 std::fs::write(pid_path, pid.to_string())?;
237 Ok(())
238}
239
240pub fn read_pid_file() -> Result<Option<u32>> {
241 let pid_path = get_pid_file_path()?;
242 if !pid_path.exists() {
243 return Ok(None);
244 }
245
246 let contents = std::fs::read_to_string(pid_path)?;
247 let pid = contents.trim().parse::<u32>()?;
248 Ok(Some(pid))
249}
250
251pub fn remove_pid_file() -> Result<()> {
252 let pid_path = get_pid_file_path()?;
253 if pid_path.exists() {
254 std::fs::remove_file(pid_path)?;
255 }
256 Ok(())
257}
258
259pub fn is_daemon_running() -> bool {
260 if let Ok(Some(pid)) = read_pid_file() {
261 #[cfg(unix)]
263 {
264 use std::process::Command;
265 if let Ok(output) = Command::new("kill").arg("-0").arg(pid.to_string()).output() {
266 return output.status.success();
267 }
268 }
269
270 #[cfg(windows)]
271 {
272 use std::process::Command;
273 if let Ok(output) = Command::new("tasklist")
274 .arg("/FI")
275 .arg(format!("PID eq {}", pid))
276 .arg("/NH")
277 .output()
278 {
279 let output_str = String::from_utf8_lossy(&output.stdout);
280 return output_str.contains(&pid.to_string());
281 }
282 }
283 }
284
285 false
286}