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