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