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    GetSessionMetrics(i64),
33
34    // Real-time monitoring
35    SubscribeToUpdates,
36    UnsubscribeFromUpdates,
37    ActivityHeartbeat,
38
39    // Project switching
40    SwitchProject(i64),
41    ListProjects,
42
43    // Daemon control
44    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, // seconds
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SessionMetrics {
85    pub session_id: i64,
86    pub active_duration: i64, // seconds
87    pub total_duration: i64,  // seconds
88    pub paused_duration: i64, // seconds
89    pub activity_score: f64,  // 0.0 to 1.0
90    pub last_activity: chrono::DateTime<chrono::Utc>,
91    pub productivity_rating: Option<u8>, // 1-5 scale
92}
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, // Change in active time since last update
100}
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        // Remove existing socket file if it exists
120        if socket_path.exists() {
121            std::fs::remove_file(socket_path)?;
122        }
123
124        // Create parent directory if it doesn't exist
125        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        // Set socket permissions (Unix only)
132        #[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        // Serialize message
170        let serialized = serde_json::to_vec(message)?;
171        let len = serialized.len() as u32;
172
173        // Send length prefix + message
174        stream.write_u32(len).await?;
175        stream.write_all(&serialized).await?;
176
177        // Read response
178        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        // Deserialize response
183        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        // Check if process is actually running
246        #[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}