Added handling of zombie processes in case of crashes and streamlined API and certificate generation.

This commit is contained in:
PaulKoudelka 2026-01-16 20:48:09 +01:00
parent 1dcfd19f72
commit 8d850d88fb
12 changed files with 466 additions and 42 deletions

View File

@ -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"),

View File

@ -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

View File

@ -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::<bool>() {
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<dyn std::error::Error>> {
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(())
}

View File

@ -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

View File

@ -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<Mutex<Option<Window>>> = 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();
},

View File

@ -2,7 +2,13 @@ use log::info;
use rcgen::generate_simple_self_signed;
use sha2::{Sha256, Digest};
pub fn generate_certificate() -> (Vec<u8>, Vec<u8>, String) {
pub struct Certificate {
pub certificate: Vec<u8>,
pub private_key: Vec<u8>,
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<u8>, Vec<u8>, 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()
}
}

View File

@ -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<u16> = Lazy::new(|| get_available_port().unwrap(
static DOTNET_INITIALIZED: Lazy<Mutex<bool>> = 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);
}
}

View File

@ -15,4 +15,5 @@ pub mod pdfium;
pub mod pandoc;
pub mod qdrant;
pub mod certificate_factory;
pub mod runtime_api_token;
pub mod runtime_api_token;
pub mod zombie_process_remover;

View File

@ -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<APIToken> = Lazy::new(|| {
static TMPDIR: Lazy<Mutex<Option<TempDir>>> = 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<ProvideQdrantInfo> {
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<dyn std::error::Error>> {
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<dyn Error>> {
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<dy
let key_path = temp_dir.join("key.pem");
let mut cert_file = File::create(&cert_path)?;
cert_file.write_all(&*certificate)?;
cert_file.write_all(&*cert.certificate)?;
let mut key_file = File::create(&key_path)?;
key_file.write_all(&*cert_private_key)?;
key_file.write_all(&*cert.private_key)?;
CERTIFICATE_FINGERPRINT.set(cert_fingerprint).expect("Could not set the certificate fingerprint.");
CERTIFICATE_FINGERPRINT.set(cert.fingerprint).expect("Could not set the certificate fingerprint.");
Ok((cert_path, key_path))
}
@ -165,4 +181,36 @@ pub fn drop_tmpdir() {
let mut guard = TMPDIR.lock().unwrap();
*guard = None;
warn!(Source = "Qdrant"; "Temporary directory for TLS was dropped.");
}
/// Remove old Pid files and kill the corresponding processes
pub fn cleanup_qdrant() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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(())
}

View File

@ -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.");
}

View File

@ -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);
}
}
}

View File

@ -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/*"