Skip to main content

snipt_server/server/
http_server.rs

1//! HTTP server implementation for the snipt API.
2
3use crate::api::{
4    add_snippet_handler, delete_snippet_handler, get_daemon_details, get_daemon_status,
5    get_snippet, get_snippets, update_snippet_handler, DeleteSnippetRequest, GetSnippetRequest,
6    SnippetRequest,
7};
8use crate::server::utils::{port_is_available, save_api_port};
9
10use snipt_core::{get_config_dir, is_daemon_running, Result, SniptError};
11use std::fs;
12use std::net::SocketAddr;
13use warp::Filter;
14
15use super::utils::get_api_server_port;
16
17/// Start the HTTP API server on the specified port
18pub async fn start_api_server(port: u16) -> Result<()> {
19    let addr = SocketAddr::from(([127, 0, 0, 1], port));
20
21    // Save the port to file so we can find it later
22    save_api_port(port)?;
23
24    println!("┌─────────────────────────────────────────┐");
25    println!("│          snipt API Server              │");
26    println!("├─────────────────────────────────────────┤");
27    println!("│ Status: Running                         │");
28    println!("│ Port:   {:<33} │", port);
29    println!("│ URL:    http://localhost:{:<21} │", port);
30    println!("└─────────────────────────────────────────┘");
31
32    // CORS for development
33    let cors = warp::cors()
34        .allow_any_origin()
35        .allow_headers(vec!["Content-Type"])
36        .allow_methods(vec!["GET", "POST", "DELETE", "PUT"]);
37
38    // API routes
39    let get_snippets_route = warp::path!("api" / "snippets")
40        .and(warp::get())
41        .map(|| warp::reply::json(&get_snippets()));
42
43    let get_snippet_route = warp::path!("api" / "snippet")
44        .and(warp::get())
45        .and(warp::query::<GetSnippetRequest>())
46        .map(|query: GetSnippetRequest| warp::reply::json(&get_snippet(&query.shortcut)));
47
48    let add_snippet_route = warp::path!("api" / "snippets")
49        .and(warp::post())
50        .and(warp::body::json())
51        .map(|body: SnippetRequest| {
52            warp::reply::json(&add_snippet_handler(body.shortcut, body.snippet))
53        });
54
55    let update_snippet_route = warp::path!("api" / "snippets")
56        .and(warp::put())
57        .and(warp::body::json())
58        .map(|body: SnippetRequest| {
59            warp::reply::json(&update_snippet_handler(body.shortcut, body.snippet))
60        });
61
62    let delete_snippet_route = warp::path!("api" / "snippets")
63        .and(warp::delete())
64        .and(warp::query::<DeleteSnippetRequest>())
65        .map(|query: DeleteSnippetRequest| {
66            warp::reply::json(&delete_snippet_handler(query.shortcut))
67        });
68
69    let daemon_status_route = warp::path!("api" / "daemon" / "status")
70        .and(warp::get())
71        .map(|| warp::reply::json(&get_daemon_status()));
72
73    let daemon_details_route = warp::path!("api" / "daemon" / "details")
74        .and(warp::get())
75        .map(move || warp::reply::json(&get_daemon_details(port)));
76
77    // Health check endpoint
78    let health_route = warp::path!("health").map(|| "snipt API is running");
79
80    // Combine routes
81    let routes = get_snippets_route
82        .or(get_snippet_route)
83        .or(add_snippet_route)
84        .or(update_snippet_route)
85        .or(delete_snippet_route)
86        .or(daemon_status_route)
87        .or(daemon_details_route)
88        .or(health_route)
89        .with(cors);
90
91    // Use warp's TcpListener creation to handle binding errors gracefully
92    let server = warp::serve(routes).try_bind_with_graceful_shutdown(addr, async {
93        // Set up shutdown signal handler
94        tokio::signal::ctrl_c().await.ok();
95        println!("Received shutdown signal, stopping API server...");
96    });
97
98    match server {
99        Ok((addr, server)) => {
100            println!("API server started successfully on {}", addr);
101
102            // Actually run the server - this will block until shutdown
103            server.await;
104            Ok(())
105        }
106        Err(e) => Err(SniptError::Other(format!(
107            "Failed to bind to port {}: {}",
108            port, e
109        ))),
110    }
111}
112
113/// Check the health of a running API server
114pub fn check_api_server_health() -> Result<()> {
115    match get_api_server_port() {
116        Ok(port) => {
117            println!("Checking API server on port {}...", port);
118
119            // Try to connect to the API server using standard TCP
120            match std::net::TcpStream::connect(format!("127.0.0.1:{}", port)) {
121                Ok(_) => {
122                    println!("✅ API server is running on port {}", port);
123                    println!("Connection successful (TCP port is open)");
124                    Ok(())
125                }
126                Err(e) => {
127                    println!("❌ Failed to connect to API server on port {}", port);
128                    println!("Error: {}", e);
129                    Err(SniptError::Other(format!(
130                        "Failed to connect to API server: {}",
131                        e
132                    )))
133                }
134            }
135        }
136        Err(_) => {
137            println!("❌ API server port information not found");
138            println!(
139                "The API server may not be running or was started without saving port information."
140            );
141            Err(SniptError::Other(
142                "API server port information not found".to_string(),
143            ))
144        }
145    }
146}
147
148/// Attempt to stop any running API server process
149pub fn stop_api_server() -> Result<()> {
150    // Try to get the port
151    if let Ok(port) = get_api_server_port() {
152        println!("Stopping API server on port {}...", port);
153
154        // Try to connect to signal shutdown
155        let _ = std::net::TcpStream::connect(format!("127.0.0.1:{}", port));
156
157        // Remove the port file
158        let port_file_path = get_config_dir().join("api_port.txt");
159        if port_file_path.exists() {
160            let _ = fs::remove_file(port_file_path);
161        }
162
163        println!("API server port file removed.");
164
165        // Try to kill processes listening on that port (platform-specific)
166        #[cfg(unix)]
167        {
168            use std::process::Command;
169            // Try using lsof to find and kill process using that port
170            let _ = Command::new("bash")
171                .arg("-c")
172                .arg(format!("lsof -ti:{} | xargs kill -9", port))
173                .status();
174        }
175
176        #[cfg(windows)]
177        {
178            use std::process::Command;
179            // Try using netstat and taskkill on Windows
180            let _ = Command::new("cmd")
181                .arg("/C")
182                .arg(format!("for /f \"tokens=5\" %a in ('netstat -aon ^| findstr :{} ^| findstr LISTENING') do taskkill /F /PID %a", port))
183                .status();
184        }
185    }
186
187    Ok(())
188}
189
190/// Run a diagnostic on the API server
191pub fn diagnose_api_server() -> Result<()> {
192    println!("snipt API Server Diagnostics");
193    println!("============================");
194
195    // Check if daemon is running
196    match is_daemon_running()? {
197        Some(pid) => println!("✅ Daemon is running with PID {}", pid),
198        None => println!("❌ Daemon is not running"),
199    }
200
201    // Check if port file exists
202    let port_file = get_config_dir().join("api_port.txt");
203    if port_file.exists() {
204        println!("✅ API port file exists at {}", port_file.display());
205
206        // Check the port
207        match std::fs::read_to_string(&port_file) {
208            Ok(content) => {
209                match content.trim().parse::<u16>() {
210                    Ok(port) => {
211                        println!("✅ Port file contains valid port: {}", port);
212
213                        // Check if the port is in use
214                        match std::net::TcpStream::connect(format!("127.0.0.1:{}", port)) {
215                            Ok(_) => {
216                                println!("✅ API server is responsive on port {}", port);
217                                println!("✅ Server URL: http://localhost:{}", port);
218                            }
219                            Err(_) => {
220                                println!(
221                                    "❌ Port {} is not in use - API server may not be running",
222                                    port
223                                );
224
225                                // Check if the port is available
226                                if port_is_available(port) {
227                                    println!("✅ Port {} is available", port);
228                                    println!("ℹ️  You can start the API server with: snipt serve --port {}", port);
229                                } else {
230                                    println!(
231                                        "❌ Port {} is in use but not responding as API server",
232                                        port
233                                    );
234                                    println!(
235                                        "ℹ️  You may need to free this port or choose another one"
236                                    );
237                                }
238                            }
239                        }
240                    }
241                    Err(_) => println!("❌ Port file contains invalid port number"),
242                }
243            }
244            Err(e) => println!("❌ Failed to read port file: {}", e),
245        }
246    } else {
247        println!("❌ API port file does not exist at {}", port_file.display());
248        println!("ℹ️  API server may not have been started, or the file was deleted");
249    }
250
251    // Check API server logs if they exist
252    let log_file = get_config_dir().join("api_server_log.txt");
253    if log_file.exists() {
254        println!("✅ API server log file exists at {}", log_file.display());
255
256        // Display the last few lines of the log
257        #[cfg(unix)]
258        {
259            use std::process::Command;
260            println!("\nLast 10 lines of API server log:");
261            let _ = Command::new("sh")
262                .arg("-c")
263                .arg(format!("tail -n 10 \"{}\"", log_file.display()))
264                .status();
265        }
266
267        #[cfg(windows)]
268        {
269            use std::process::Command;
270            println!("\nLast 5 lines of API server log:");
271            let _ = Command::new("cmd")
272                .arg("/C")
273                .arg(format!(
274                    "type \"{}\" | findstr /n . | findstr /r \"[1-5]:\"",
275                    log_file.display()
276                ))
277                .status();
278        }
279    } else {
280        println!(
281            "❌ API server log file does not exist at {}",
282            log_file.display()
283        );
284    }
285
286    println!("\nTo start the API server, you can run: snipt serve --port <port>");
287    println!("To restart daemon and API: snipt start");
288    println!("To stop all services: snipt stop");
289
290    Ok(())
291}