tempo_cli/utils/
ipc.rs

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    // Project tracking
10    ProjectEntered {
11        path: PathBuf,
12        context: String,
13    },
14    ProjectLeft {
15        path: PathBuf,
16    },
17
18    // Session control
19    StartSession {
20        project_path: Option<PathBuf>,
21        context: String,
22    },
23    StopSession,
24    PauseSession,
25    ResumeSession,
26
27    // Status queries
28    GetStatus,
29    GetActiveSession,
30    GetProject(i64),
31    GetDailyStats(chrono::NaiveDate),
32    GetWeeklyStats,
33    GetSessionsForDate(chrono::NaiveDate),
34    GetSessionMetrics(i64),
35    GetRecentProjects,
36
37    // Real-time monitoring
38    SubscribeToUpdates,
39    UnsubscribeFromUpdates,
40    ActivityHeartbeat,
41
42    // Project switching
43    SwitchProject(i64),
44    ListProjects,
45
46    // Daemon control
47    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, // seconds
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SessionMetrics {
101    pub session_id: i64,
102    pub active_duration: i64, // seconds
103    pub total_duration: i64,  // seconds
104    pub paused_duration: i64, // seconds
105    pub activity_score: f64,  // 0.0 to 1.0
106    pub last_activity: chrono::DateTime<chrono::Utc>,
107    pub productivity_rating: Option<u8>, // 1-5 scale
108}
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, // Change in active time since last update
116}
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        // Remove existing socket file if it exists
136        if socket_path.exists() {
137            std::fs::remove_file(socket_path)?;
138        }
139
140        // Create parent directory if it doesn't exist
141        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        // Set socket permissions (Unix only)
148        #[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        // Serialize message
186        let serialized = serde_json::to_vec(message)?;
187        let len = serialized.len() as u32;
188
189        // Send length prefix + message
190        stream.write_u32(len).await?;
191        stream.write_all(&serialized).await?;
192
193        // Read response
194        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        // Deserialize response
199        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        // Check if process is actually running
262        #[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}