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 { path: PathBuf, context: String },
11    ProjectLeft { path: PathBuf },
12    
13    // Session control
14    StartSession { project_path: Option<PathBuf>, context: String },
15    StopSession,
16    PauseSession,
17    ResumeSession,
18    
19    // Status queries
20    GetStatus,
21    GetActiveSession,
22    GetProject(i64),
23    GetDailyStats(chrono::NaiveDate),
24    GetSessionMetrics(i64),
25    
26    // Real-time monitoring
27    SubscribeToUpdates,
28    UnsubscribeFromUpdates,
29    ActivityHeartbeat,
30    
31    // Project switching
32    SwitchProject(i64),
33    
34    // Daemon control
35    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, // seconds
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SessionMetrics {
75    pub session_id: i64,
76    pub active_duration: i64, // seconds
77    pub total_duration: i64,  // seconds
78    pub paused_duration: i64, // seconds
79    pub activity_score: f64,  // 0.0 to 1.0
80    pub last_activity: chrono::DateTime<chrono::Utc>,
81    pub productivity_rating: Option<u8>, // 1-5 scale
82}
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, // Change in active time since last update
90}
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        // Remove existing socket file if it exists
110        if socket_path.exists() {
111            std::fs::remove_file(socket_path)?;
112        }
113
114        // Create parent directory if it doesn't exist
115        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        // Set socket permissions (Unix only)
122        #[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        // Serialize message
155        let serialized = serde_json::to_vec(message)?;
156        let len = serialized.len() as u32;
157
158        // Send length prefix + message
159        stream.write_u32(len).await?;
160        stream.write_all(&serialized).await?;
161
162        // Read response
163        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        // Deserialize response
168        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        // Check if process is actually running
231        #[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}