diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs index 4133332e..923b5a47 100644 --- a/app/Build/Commands/Qdrant.cs +++ b/app/Build/Commands/Qdrant.cs @@ -62,19 +62,19 @@ public static class Qdrant return; } - var qdrantDBSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path); - var qdrantDBTargetPath = Path.Join(cwd, "resources", "databases", "qdrant",database.Filename); - if (!File.Exists(qdrantDBSourcePath)) + var qdrantDbSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path); + var qdrantDbTargetPath = Path.Join(cwd, "target", "databases", "qdrant",database.Filename); + if (!File.Exists(qdrantDbSourcePath)) { - Console.WriteLine($" failed to find the database file '{qdrantDBSourcePath}'"); + Console.WriteLine($" failed to find the database file '{qdrantDbSourcePath}'"); return; } - Directory.CreateDirectory(Path.Join(cwd, "resources", "databases", "qdrant")); - if (File.Exists(qdrantDBTargetPath)) - File.Delete(qdrantDBTargetPath); + Directory.CreateDirectory(Path.Join(cwd, "target", "databases", "qdrant")); + if (File.Exists(qdrantDbTargetPath)) + File.Delete(qdrantDbTargetPath); - File.Copy(qdrantDBSourcePath, qdrantDBTargetPath); + File.Copy(qdrantDbSourcePath, qdrantDbTargetPath); // // Cleanup: @@ -91,7 +91,7 @@ public static class Qdrant RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"), RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"), - RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-musl"), + RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"), RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"), RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"), diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a518b32d..bb28ee6e 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -7,6 +7,7 @@ authors = ["Thorsten Sommer"] [build-dependencies] tauri-build = { version = "1.5", features = [] } +dirs = "6.0.0" [dependencies] tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog"] } @@ -46,6 +47,7 @@ url = "2.5" ring = "0.17.14" crossbeam-channel = "0.5.15" tracing-subscriber = "0.3.20" +dirs = "6.0.0" [target.'cfg(target_os = "linux")'.dependencies] # See issue https://github.com/tauri-apps/tauri/issues/4470 diff --git a/runtime/build.rs b/runtime/build.rs index 93871d1a..afa013cf 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -1,6 +1,39 @@ -use std::path::PathBuf; +use std::{env, fs}; +use std::path::{PathBuf}; +use std::process::Command; +use std::io::{Error, ErrorKind}; fn main() { + match env::var("MINDWORK_START_DEV_ENV") { + Ok(val) => { + let is_started_manually = match val.parse::() { + Ok(b) => b, + Err(_) => { + println!("cargo: warning= Invalid value for MINDWORK_START_DEV_ENV: expected 'true' or 'false'"); + return; + } + }; + if is_started_manually { + if let Err(e) = kill_zombie_qdrant_process(){ + println!("cargo:warning=Error: {e}"); + return; + }; + if let Err(e) = delete_old_certificates() { + println!("cargo: warning= Failed to delete old certificates: {e}"); + } + } + }, + Err(_) => { + println!("cargo: warning= The environment variable 'MINDWORK_START_DEV_ENV' was not found."); + if let Err(e) = kill_zombie_qdrant_process(){ + println!("cargo:warning=Error: {e}"); + return; + }; + if let Err(e) = delete_old_certificates() { + println!("cargo: warning= Failed to delete old certificates: {e}"); + } + } + } tauri_build::build(); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); @@ -84,3 +117,166 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) { std::fs::write(tauri_conf_path, new_tauri_conf).unwrap(); } + +#[cfg(unix)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let ps_output = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .arg("-o") + .arg("comm=") + .output()?; + + let output = String::from_utf8_lossy(&ps_output.stdout).trim().to_string(); + + if output.is_empty() { + // Process doesn't exist + return Ok(()); + } + + let name = output; + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, "Process name does not match")); + } + + // + // Kill the process + // + let kill_output = Command::new("kill") + .arg("-9") + .arg(pid.to_string()) + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let ps_check = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .output()?; + + let output = String::from_utf8_lossy(&ps_check.stdout).trim().to_string(); + if output.is_empty() { + Ok(()) + } else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + +#[cfg(windows)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let tasklist_output = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .arg("/FO") + .arg("CSV") + .arg("/NH") + .output()?; + + let output = String::from_utf8_lossy(&tasklist_output.stdout).trim().to_string(); + + if output.is_empty() || !output.starts_with('"') { + println!("cargo:warning= Pid file was found, but process was not."); + return Ok(()) + } + + let name = output.split(',').next().unwrap_or("").trim_matches('"'); + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, format!("Process name does not match. Expected:{}, got:{}",expected_name,name))); + } + + // + // Kill the process + // + let kill_output = Command::new("taskkill") + .arg("/PID") + .arg(pid.to_string()) + .arg("/F") + .arg("/T") + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let tasklist_check = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .output()?; + + let output = String::from_utf8_lossy(&tasklist_check.stdout).trim().to_string(); + if output.is_empty() || !output.starts_with('"') { + Ok(()) + } + else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + + +pub fn kill_zombie_qdrant_process() -> Result<(), Error> { + let pid_file = dirs::data_local_dir() + .expect("Local appdata was not found") + .join("com.github.mindwork-ai.ai-studio") + .join("data") + .join("databases") + .join("qdrant") + .join("qdrant.pid"); + + if !pid_file.exists() { + return Ok(()); + } + + let pid_str = fs::read_to_string(&pid_file)?; + let pid: u32 = pid_str.trim().parse().map_err(|_| {Error::new(ErrorKind::InvalidData, "Invalid PID in file")})?; + if let Err(e) = ensure_process_killed(pid, "qdrant.exe".as_ref()){ + return Err(e); + } + + fs::remove_file(&pid_file)?; + println!("cargo:warning= Killed qdrant process and deleted redundant Pid file: {}", pid_file.display()); + + Ok(()) +} + +pub fn delete_old_certificates() -> Result<(), Box> { + let dir_path = dirs::data_local_dir() + .expect("Local appdata was not found") + .join("com.github.mindwork-ai.ai-studio") + .join("data") + .join("databases") + .join("qdrant"); + + if !dir_path.exists() { + return Ok(()); + } + + for entry in fs::read_dir(dir_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + let file_name = entry.file_name(); + let folder_name = file_name.to_string_lossy(); + + if folder_name.starts_with("cert-") { + fs::remove_dir_all(&path)?; + println!("cargo: warning= Removed old certificates in: {}", path.display()); + } + } + } + Ok(()) +} diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml index 1149a0ec..50f03e08 100644 --- a/runtime/resources/databases/qdrant/config.yaml +++ b/runtime/resources/databases/qdrant/config.yaml @@ -249,10 +249,11 @@ service: # If enabled, browsers would be allowed to query REST endpoints regardless of query origin. # More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS # Default: true - enable_cors: true + enable_cors: false # Enable HTTPS for the REST and gRPC API - enable_tls: false + # TLS is enabled in AI Studio through environment variables when instantiating Qdrant as a sidecar. + # enable_tls: false # Check user HTTPS client certificate against CA file specified in tls config verify_https_client_certificate: false diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 69661553..dbb521b6 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::sync::Mutex; use std::time::Duration; use log::{debug, error, info, trace, warn}; @@ -8,16 +9,16 @@ use rocket::serde::json::Json; use rocket::serde::Serialize; use serde::Deserialize; use tauri::updater::UpdateResponse; -use tauri::{FileDropEvent, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent}; +use tauri::{FileDropEvent, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent, generate_context}; use tauri::api::dialog::blocking::FileDialogBuilder; use tokio::sync::broadcast; use tokio::time; use crate::api_token::APIToken; -use crate::dotnet::stop_dotnet_server; +use crate::dotnet::{cleanup_dotnet_server, stop_dotnet_server, PID_FILE_NAME}; use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY}; use crate::log::switch_to_file_logging; use crate::pdfium::PDFIUM_LIB_PATH; -use crate::qdrant::{start_qdrant_server, stop_qdrant_server}; +use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server}; /// The Tauri main window. static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); @@ -92,16 +93,21 @@ pub fn start_tauri() { DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not abe to set the data directory.")).unwrap(); CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap(); + if is_prod() { + cleanup_qdrant().expect("Zombie processes of Qdrant were not killed"); + cleanup_dotnet_server(); + } + info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap(); set_pdfium_path(app.path_resolver()); - + start_qdrant_server(); - + Ok(()) }) .plugin(tauri_plugin_window_state::Builder::default().build()) - .build(tauri::generate_context!()) + .build(generate_context!()) .expect("Error while running Tauri application"); // The app event handler: @@ -438,6 +444,7 @@ pub async fn install_update(_token: APIToken) { let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone(); match cloned_response_option { Some(update_response) => { + stop_qdrant_server(); update_response.download_and_install().await.unwrap(); }, diff --git a/runtime/src/certificate_factory.rs b/runtime/src/certificate_factory.rs index 3c30d34a..c7dad76e 100644 --- a/runtime/src/certificate_factory.rs +++ b/runtime/src/certificate_factory.rs @@ -2,7 +2,13 @@ use log::info; use rcgen::generate_simple_self_signed; use sha2::{Sha256, Digest}; -pub fn generate_certificate() -> (Vec, Vec, String) { +pub struct Certificate { + pub certificate: Vec, + pub private_key: Vec, + pub fingerprint: String, +} + +pub fn generate_certificate() -> Certificate { let subject_alt_names = vec!["localhost".to_string()]; let certificate_data = generate_simple_self_signed(subject_alt_names).unwrap(); @@ -18,5 +24,9 @@ pub fn generate_certificate() -> (Vec, Vec, String) { info!("Certificate fingerprint: '{certificate_fingerprint}'."); - (certificate_data.cert.pem().as_bytes().to_vec(), certificate_data.signing_key.serialize_pem().as_bytes().to_vec(), certificate_fingerprint.clone()) + Certificate { + certificate: certificate_data.cert.pem().as_bytes().to_vec(), + private_key: certificate_data.signing_key.serialize_pem().as_bytes().to_vec(), + fingerprint: certificate_fingerprint.clone() + } } \ No newline at end of file diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index fb792a15..7d2bc9c8 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::Path; use std::sync::{Arc, Mutex}; use base64::Engine; use base64::prelude::BASE64_STANDARD; @@ -12,9 +13,10 @@ use crate::runtime_api_token::API_TOKEN; use crate::app_window::change_location_to; use crate::runtime_certificate::CERTIFICATE_FINGERPRINT; use crate::encryption::ENCRYPTION; -use crate::environment::is_dev; +use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::network::get_available_port; use crate::runtime_api::API_SERVER_PORT; +use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; // The .NET server is started in a separate process and communicates with this // runtime process via IPC. However, we do net start the .NET server in @@ -27,6 +29,8 @@ static DOTNET_SERVER_PORT: Lazy = Lazy::new(|| get_available_port().unwrap( static DOTNET_INITIALIZED: Lazy> = Lazy::new(|| Mutex::new(false)); +pub const PID_FILE_NAME: &str = "mindwork_ai_studio.pid"; + /// Returns the desired port of the .NET server. Our .NET app calls this endpoint to get /// the port where the .NET server should listen to. #[get("/system/dotnet/port")] @@ -94,9 +98,9 @@ pub fn start_dotnet_server() { .envs(dotnet_server_environment) .spawn() .expect("Failed to spawn .NET server process."); - let server_pid = child.pid(); info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); + log_potential_zombie_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid.to_string().as_str()); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); @@ -141,6 +145,7 @@ pub fn start_dotnet_server() { } } }); + } /// This endpoint is called by the .NET server to signal that the server is ready. @@ -185,4 +190,13 @@ pub fn stop_dotnet_server() { } else { warn!("The .NET server process was not started or is already stopped."); } + cleanup_dotnet_server(); +} + +/// Remove old Pid files and kill the corresponding processes +pub fn cleanup_dotnet_server() { + let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME); + if let Err(e) = kill_zombie_process(pid_path, "mindworkAIStudioServer.exe"){ + warn!(Source = ".NET"; "Error during the cleanup of .NET: {}", e); + } } \ No newline at end of file diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e99f528b..65b6abdf 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -15,4 +15,5 @@ pub mod pdfium; pub mod pandoc; pub mod qdrant; pub mod certificate_factory; -pub mod runtime_api_token; \ No newline at end of file +pub mod runtime_api_token; +pub mod zombie_process_remover; \ No newline at end of file diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 4a945d5d..3dac0e8c 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::{fs}; +use std::error::Error; use std::fs::File; use std::io::Write; use std::path::Path; @@ -14,6 +16,7 @@ use crate::environment::DATA_DIRECTORY; use crate::certificate_factory::generate_certificate; use std::path::PathBuf; use tempfile::{TempDir, Builder}; +use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; // Qdrant server process started in a separate process and can communicate // via HTTP or gRPC with the .NET server and the runtime process @@ -35,6 +38,8 @@ static API_TOKEN: Lazy = Lazy::new(|| { static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); +const PID_FILE_NAME: &str = "qdrant.pid"; + #[derive(Serialize)] pub struct ProvideQdrantInfo { path: String, @@ -46,24 +51,30 @@ pub struct ProvideQdrantInfo { #[get("/system/qdrant/info")] pub fn qdrant_port(_token: APIToken) -> Json { - return Json(ProvideQdrantInfo { + Json(ProvideQdrantInfo { path: Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").to_str().unwrap().to_string(), port_http: *QDRANT_SERVER_PORT_HTTP, port_grpc: *QDRANT_SERVER_PORT_GRPC, fingerprint: CERTIFICATE_FINGERPRINT.get().expect("Certificate fingerprint not available").to_string(), api_token: API_TOKEN.to_hex_text().to_string(), - }); + }) } /// Starts the Qdrant server in a separate process. -pub fn start_qdrant_server() { +pub fn start_qdrant_server(){ let base_path = DATA_DIRECTORY.get().unwrap(); - let (cert_path, key_path) =create_temp_tls_files(Path::new(base_path).join("databases").join("qdrant")).unwrap(); + let path = Path::new(base_path).join("databases").join("qdrant"); + if !path.exists() { + if let Err(e) = fs::create_dir_all(&path){ + error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e.to_string()); + }; + } + let (cert_path, key_path) =create_temp_tls_files(&path).unwrap(); - let storage_path = Path::new(base_path).join("databases").join("qdrant").join("storage").to_str().unwrap().to_string(); - let snapshot_path = Path::new(base_path).join("databases").join("qdrant").join("snapshots").to_str().unwrap().to_string(); - let init_path = Path::new(base_path).join("databases").join("qdrant").join(".qdrant-initalized").to_str().unwrap().to_string(); + let storage_path = path.join("storage").to_str().unwrap().to_string(); + let snapshot_path = path.join("snapshots").to_str().unwrap().to_string(); + let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string(); let qdrant_server_environment = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), @@ -88,6 +99,7 @@ pub fn start_qdrant_server() { let server_pid = child.pid(); info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); + log_potential_zombie_process(path.join(PID_FILE_NAME), server_pid.to_string().as_str()); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); @@ -118,7 +130,6 @@ pub fn start_qdrant_server() { /// Stops the Qdrant server process. pub fn stop_qdrant_server() { - drop_tmpdir(); if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { let server_kill_result = server_process.kill(); match server_kill_result { @@ -128,10 +139,15 @@ pub fn stop_qdrant_server() { } else { warn!(Source = "Qdrant"; "Qdrant server process was not started or is already stopped."); } + drop_tmpdir(); + if let Err(e) = cleanup_qdrant(){ + warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); + } } -pub fn create_temp_tls_files(path: PathBuf) -> Result<(PathBuf, PathBuf), Box> { - let (certificate, cert_private_key, cert_fingerprint) = generate_certificate(); +/// Create temporary directory with TLS relevant files +pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box> { + let cert = generate_certificate(); let temp_dir = init_tmpdir_in(path); @@ -139,12 +155,12 @@ pub fn create_temp_tls_files(path: PathBuf) -> Result<(PathBuf, PathBuf), Box Result<(), Box> { + let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME); + kill_zombie_process(pid_path, "qdrant.exe")?; + delete_old_certificates()?; + Ok(()) +} + +pub fn delete_old_certificates() -> Result<(), Box> { + let dir_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant"); + + if !dir_path.exists() { + return Ok(()); + } + + for entry in fs::read_dir(dir_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + let file_name = entry.file_name(); + let folder_name = file_name.to_string_lossy(); + + if folder_name.starts_with("cert-") { + fs::remove_dir_all(&path)?; + warn!(Source="Qdrant"; "Removed old certificates in: {}", path.display()); + } + } + } + Ok(()) } \ No newline at end of file diff --git a/runtime/src/runtime_certificate.rs b/runtime/src/runtime_certificate.rs index abbde65c..e4255861 100644 --- a/runtime/src/runtime_certificate.rs +++ b/runtime/src/runtime_certificate.rs @@ -16,11 +16,11 @@ pub fn generate_runtime_certificate() { info!("Try to generate a TLS certificate for the runtime API server..."); - let (certificate, cer_private_key, cer_fingerprint) = generate_certificate(); + let cert = generate_certificate(); - CERTIFICATE_FINGERPRINT.set(cer_fingerprint).expect("Could not set the certificate fingerprint."); - CERTIFICATE.set(certificate).expect("Could not set the certificate."); - CERTIFICATE_PRIVATE_KEY.set(cer_private_key).expect("Could not set the private key."); + CERTIFICATE_FINGERPRINT.set(cert.fingerprint).expect("Could not set the certificate fingerprint."); + CERTIFICATE.set(cert.certificate).expect("Could not set the certificate."); + CERTIFICATE_PRIVATE_KEY.set(cert.private_key).expect("Could not set the private key."); info!("Done generating certificate for the runtime API server."); } \ No newline at end of file diff --git a/runtime/src/zombie_process_remover.rs b/runtime/src/zombie_process_remover.rs new file mode 100644 index 00000000..33d2ba74 --- /dev/null +++ b/runtime/src/zombie_process_remover.rs @@ -0,0 +1,145 @@ +use std::fs; +use std::fs::File; +use std::io::{Error, ErrorKind, Write}; +use std::path::PathBuf; +use std::process::Command; +use log::{info, warn}; + +#[cfg(unix)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let ps_output = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .arg("-o") + .arg("comm=") + .output()?; + + let output = String::from_utf8_lossy(&ps_output.stdout).trim().to_string(); + + if output.is_empty() { + info!("Pid file {} was found, but process was not.", pid); + return Ok(()); + } + + let name = output; + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, "Process name does not match")); + } + + // + // Kill the process + // + let kill_output = Command::new("kill") + .arg("-9") + .arg(pid.to_string()) + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let ps_check = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .output()?; + + let output = String::from_utf8_lossy(&ps_check.stdout).trim().to_string(); + if output.is_empty() { + Ok(()) + } else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + +#[cfg(windows)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let tasklist_output = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .arg("/FO") + .arg("CSV") + .arg("/NH") + .output()?; + + let output = String::from_utf8_lossy(&tasklist_output.stdout).trim().to_string(); + + if output.is_empty() || !output.starts_with('"') { + info!("Pid file {} was found, but process was not.", pid); + return Ok(()) + } + + let name = output.split(',').next().unwrap_or("").trim_matches('"'); + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, format!("Process name does not match. Expected:{}, got:{}",expected_name,name))); + } + + // + // Kill the process + // + let kill_output = Command::new("taskkill") + .arg("/PID") + .arg(pid.to_string()) + .arg("/F") + .arg("/T") + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let tasklist_check = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .output()?; + + let output = String::from_utf8_lossy(&tasklist_check.stdout).trim().to_string(); + if output.is_empty() || !output.starts_with('"') { + Ok(()) + } + else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + + +pub fn kill_zombie_process(pid_file_path: PathBuf, process_name: &str) -> Result<(), Error> { + if !pid_file_path.exists() { + return Ok(()); + } + + let pid_str = fs::read_to_string(&pid_file_path)?; + let pid: u32 = pid_str.trim().parse().map_err(|_| {Error::new(ErrorKind::InvalidData, "Invalid PID in file")})?; + if let Err(e) = ensure_process_killed(pid, process_name){ + return Err(e); + } + + fs::remove_file(&pid_file_path)?; + info!("Killed qdrant process and deleted redundant Pid file: {}", pid_file_path.display()); + + Ok(()) +} + +pub fn log_potential_zombie_process(pid_file_path: PathBuf, content: &str) { + match File::create(&pid_file_path) { + Ok(mut file) => { + if let Err(e) = file.write_all(content.as_bytes()) { + warn!("Failed to write to {}: {}", pid_file_path.display(), e); + } + } + Err(e) => { + warn!("Failed to create {}: {}", pid_file_path.display(), e); + } + } +} \ No newline at end of file diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index d2cb54f3..8b2cb703 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -22,7 +22,7 @@ "args": true }, { - "name": "resources/databases/qdrant/qdrant", + "name": "target/databases/qdrant/qdrant", "sidecar": true, "args": true } @@ -65,7 +65,7 @@ "identifier": "com.github.mindwork-ai.ai-studio", "externalBin": [ "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", - "resources/databases/qdrant/qdrant" + "target/databases/qdrant/qdrant" ], "resources": [ "resources/*"