fixed issues with stale processes

This commit is contained in:
PaulKoudelka 2026-01-30 17:59:15 +01:00
parent 91bf83ea12
commit 223d288ab4
11 changed files with 136 additions and 374 deletions

View File

@ -2,7 +2,7 @@ name: Build and Release
on: on:
push: push:
branches: branches:
- "**" - main
tags: tags:
- "v*.*.*" - "v*.*.*"
@ -631,7 +631,7 @@ jobs:
cd runtime cd runtime
export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY" export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD" export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
cargo tauri build --target ${{ matrix.rust_target }} --bundles none cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }}
- name: Build Tauri project (Windows) - name: Build Tauri project (Windows)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
@ -642,7 +642,7 @@ jobs:
cd runtime cd runtime
$env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY" $env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
$env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD" $env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
cargo tauri build --target ${{ matrix.rust_target }} --bundles none cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }}
- name: Upload artifact (macOS) - name: Upload artifact (macOS)
if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v') if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v')

View File

@ -238,6 +238,8 @@
<ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/> <ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
<ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/> <ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
<ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/> <ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/>
<ThirdPartyComponent Name="sysinfo" Developer="Guillaume Gomez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/GuillaumeGomez/sysinfo?tab=MIT-1-ov-file" RepositoryUrl="https://github.com/GuillaumeGomez/sysinfo" UseCase="@T("This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes.")"/>
<ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/Stebalien/tempfile?tab=Apache-2.0-1-ov-file" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant.")"/>
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.")" /> <ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.")" />
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="@T("We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.")"/> <ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="@T("We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="@T("This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.")"/> <ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="@T("This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>

View File

@ -7,7 +7,6 @@ authors = ["Thorsten Sommer"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.5", features = [] } tauri-build = { version = "1.5", features = [] }
dirs = "6.0.0"
[dependencies] [dependencies]
tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] }
@ -42,6 +41,7 @@ cfg-if = "1.0.4"
pptx-to-md = "0.4.0" pptx-to-md = "0.4.0"
tempfile = "3.8" tempfile = "3.8"
strum_macros = "0.27" strum_macros = "0.27"
sysinfo = "0.38.0"
# Fixes security vulnerability downstream, where the upstream is not fixed yet: # Fixes security vulnerability downstream, where the upstream is not fixed yet:
url = "2.5.8" url = "2.5.8"
@ -50,6 +50,7 @@ crossbeam-channel = "0.5.15"
tracing-subscriber = "0.3.20" tracing-subscriber = "0.3.20"
dirs = "6.0.0" dirs = "6.0.0"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
# See issue https://github.com/tauri-apps/tauri/issues/4470 # See issue https://github.com/tauri-apps/tauri/issues/4470
reqwest = { version = "0.13.1", features = ["native-tls-vendored"] } reqwest = { version = "0.13.1", features = ["native-tls-vendored"] }

View File

@ -1,32 +1,6 @@
use std::{env, fs};
use std::path::{PathBuf}; use std::path::{PathBuf};
use std::process::Command;
use std::io::{Error, ErrorKind};
fn main() { 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.");
}
}
tauri_build::build(); tauri_build::build();
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
@ -110,166 +84,3 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
std::fs::write(tauri_conf_path, new_tauri_conf).unwrap(); 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

@ -15,11 +15,13 @@ use tauri::api::dialog::blocking::FileDialogBuilder;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::time; use tokio::time;
use crate::api_token::APIToken; use crate::api_token::APIToken;
use crate::dotnet::{cleanup_dotnet_server, stop_dotnet_server}; use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_server};
use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY}; use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY};
use crate::log::switch_to_file_logging; use crate::log::switch_to_file_logging;
use crate::pdfium::PDFIUM_LIB_PATH; use crate::pdfium::PDFIUM_LIB_PATH;
use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server}; use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server};
#[cfg(debug_assertions)]
use crate::dotnet::create_startup_env_file;
/// The Tauri main window. /// The Tauri main window.
static MAIN_WINDOW: Lazy<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None)); static MAIN_WINDOW: Lazy<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None));
@ -102,20 +104,24 @@ pub fn start_tauri() {
let data_path = data_path.join("data"); let data_path = data_path.join("data");
// Get and store the data and config directories: // Get and store the data and config directories:
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not abe to set the data directory.")).unwrap(); DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able 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(); 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();
cleanup_qdrant().expect("Zombie processes of Qdrant were not killed");
cleanup_dotnet_server(); cleanup_dotnet_server();
if is_dev() {
#[cfg(debug_assertions)]
create_startup_env_file();
} else {
start_dotnet_server();
} }
start_qdrant_server();
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); 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(); switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
set_pdfium_path(app.path_resolver()); set_pdfium_path(app.path_resolver());
start_qdrant_server();
Ok(()) Ok(())
}) })
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
@ -164,6 +170,7 @@ pub fn start_tauri() {
if is_prod() { if is_prod() {
stop_dotnet_server(); stop_dotnet_server();
stop_qdrant_server();
} else { } else {
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server."); warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
} }
@ -193,6 +200,10 @@ pub fn start_tauri() {
RunEvent::ExitRequested { .. } => { RunEvent::ExitRequested { .. } => {
warn!(Source = "Tauri"; "Run event: exit was requested."); warn!(Source = "Tauri"; "Run event: exit was requested.");
stop_qdrant_server(); stop_qdrant_server();
if is_prod() {
warn!("Try to stop the .NET server as well...");
stop_dotnet_server();
}
} }
RunEvent::Ready => { RunEvent::Ready => {
@ -204,10 +215,6 @@ pub fn start_tauri() {
}); });
warn!(Source = "Tauri"; "Tauri app was stopped."); warn!(Source = "Tauri"; "Tauri app was stopped.");
if is_prod() {
warn!("Try to stop the .NET server as well...");
stop_dotnet_server();
}
} }
/// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client. /// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client.
@ -458,7 +465,6 @@ pub async fn install_update(_token: APIToken) {
let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone(); let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
match cloned_response_option { match cloned_response_option {
Some(update_response) => { Some(update_response) => {
stop_qdrant_server();
update_response.download_and_install().await.unwrap(); update_response.download_and_install().await.unwrap();
}, },

View File

@ -16,7 +16,7 @@ use crate::encryption::ENCRYPTION;
use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::environment::{is_dev, DATA_DIRECTORY};
use crate::network::get_available_port; use crate::network::get_available_port;
use crate::runtime_api::API_SERVER_PORT; use crate::runtime_api::API_SERVER_PORT;
use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
// The .NET server is started in a separate process and communicates with this // 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 // runtime process via IPC. However, we do net start the .NET server in
@ -100,7 +100,7 @@ pub fn start_dotnet_server() {
.expect("Failed to spawn .NET server process."); .expect("Failed to spawn .NET server process.");
let server_pid = child.pid(); let server_pid = child.pid();
info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_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()); log_potential_stale_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid);
// Save the server process to stop it later: // Save the server process to stop it later:
*server_spawn_clone.lock().unwrap() = Some(child); *server_spawn_clone.lock().unwrap() = Some(child);
@ -158,13 +158,14 @@ pub fn stop_dotnet_server() {
} else { } else {
warn!("The .NET server process was not started or is already stopped."); warn!("The .NET server process was not started or is already stopped.");
} }
info!("Start dotnet server cleanup");
cleanup_dotnet_server(); cleanup_dotnet_server();
} }
/// Remove old Pid files and kill the corresponding processes /// Remove old Pid files and kill the corresponding processes
pub fn cleanup_dotnet_server() { pub fn cleanup_dotnet_server() {
let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME); let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME);
if let Err(e) = kill_zombie_process(pid_path, "mindworkAIStudioServer.exe"){ if let Err(e) = kill_stale_process(pid_path) {
warn!(Source = ".NET"; "Error during the cleanup of .NET: {}", e); warn!(Source = ".NET"; "Error during the cleanup of .NET: {}", e);
} }
} }

View File

@ -16,4 +16,4 @@ pub mod pandoc;
pub mod qdrant; pub mod qdrant;
pub mod certificate_factory; pub mod certificate_factory;
pub mod runtime_api_token; pub mod runtime_api_token;
pub mod zombie_process_remover; pub mod stale_process_cleanup;

View File

@ -7,14 +7,11 @@ extern crate core;
use log::{info, warn}; use log::{info, warn};
use mindwork_ai_studio::app_window::start_tauri; use mindwork_ai_studio::app_window::start_tauri;
use mindwork_ai_studio::runtime_certificate::{generate_runtime_certificate}; use mindwork_ai_studio::runtime_certificate::{generate_runtime_certificate};
use mindwork_ai_studio::dotnet::start_dotnet_server;
use mindwork_ai_studio::environment::is_dev; use mindwork_ai_studio::environment::is_dev;
use mindwork_ai_studio::log::init_logging; use mindwork_ai_studio::log::init_logging;
use mindwork_ai_studio::metadata::MetaData; use mindwork_ai_studio::metadata::MetaData;
use mindwork_ai_studio::runtime_api::start_runtime_api; use mindwork_ai_studio::runtime_api::start_runtime_api;
#[cfg(debug_assertions)]
use mindwork_ai_studio::dotnet::create_startup_env_file;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -49,12 +46,5 @@ async fn main() {
generate_runtime_certificate(); generate_runtime_certificate();
start_runtime_api(); start_runtime_api();
if is_dev() {
#[cfg(debug_assertions)]
create_startup_env_file();
} else {
start_dotnet_server();
}
start_tauri(); start_tauri();
} }

View File

@ -16,7 +16,7 @@ use crate::environment::DATA_DIRECTORY;
use crate::certificate_factory::generate_certificate; use crate::certificate_factory::generate_certificate;
use std::path::PathBuf; use std::path::PathBuf;
use tempfile::{TempDir, Builder}; use tempfile::{TempDir, Builder};
use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
// Qdrant server process started in a separate process and can communicate // Qdrant server process started in a separate process and can communicate
// via HTTP or gRPC with the .NET server and the runtime process // via HTTP or gRPC with the .NET server and the runtime process
@ -106,7 +106,7 @@ pub fn start_qdrant_server(){
let server_pid = child.pid(); let server_pid = child.pid();
info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_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()); log_potential_stale_process(path.join(PID_FILE_NAME), server_pid);
// Save the server process to stop it later: // Save the server process to stop it later:
*server_spawn_clone.lock().unwrap() = Some(child); *server_spawn_clone.lock().unwrap() = Some(child);
@ -150,9 +150,7 @@ pub fn stop_qdrant_server() {
} }
drop_tmpdir(); drop_tmpdir();
if let Err(e) = cleanup_qdrant(){ cleanup_qdrant();
warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e);
}
} }
/// Create temporary directory with TLS relevant files /// Create temporary directory with TLS relevant files
@ -193,11 +191,15 @@ pub fn drop_tmpdir() {
} }
/// Remove old Pid files and kill the corresponding processes /// Remove old Pid files and kill the corresponding processes
pub fn cleanup_qdrant() -> Result<(), Box<dyn Error>> { pub fn cleanup_qdrant() {
let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME); let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME);
kill_zombie_process(pid_path, "qdrant.exe")?; if let Err(e) = kill_stale_process(pid_path) {
delete_old_certificates()?; warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e);
Ok(()) }
if let Err(e) = delete_old_certificates() {
warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e);
}
} }
pub fn delete_old_certificates() -> Result<(), Box<dyn Error>> { pub fn delete_old_certificates() -> Result<(), Box<dyn Error>> {

View File

@ -0,0 +1,94 @@
use std::fs;
use std::fs::File;
use std::io::{Error, ErrorKind, Write};
use std::path::{PathBuf};
use log::{info, warn};
use sysinfo::{Pid, ProcessesToUpdate, Signal, System};
fn parse_pid_file(content: &str) -> Result<(u32, String), Error> {
let mut lines = content
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty());
let pid_str = lines
.next()
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing PID in file"))?;
let pid: u32 = pid_str
.parse()
.map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid PID in file"))?;
let name = lines
.next()
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing process name in file"))?
.to_string();
Ok((pid, name))
}
pub fn kill_stale_process(pid_file_path: PathBuf) -> Result<(), Error> {
if !pid_file_path.exists() {
return Ok(());
}
let pid_file_content = fs::read_to_string(&pid_file_path)?;
let (pid, expected_name) = parse_pid_file(&pid_file_content)?;
let mut system = System::new_all();
let pid = Pid::from_u32(pid);
system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
if let Some(process) = system.process(pid){
let name = process.name().to_string_lossy();
if name != expected_name {
return Err(Error::new(
ErrorKind::InvalidInput,
format!(
"Process name does not match: expected '{}' but found '{}'",
expected_name, name
),
));
}
let killed = process.kill_with(Signal::Kill).unwrap_or_else(|| process.kill());
if !killed {
return Err(Error::new(ErrorKind::Other, "Failed to kill process"));
}
system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
if !system.process(pid).is_none() {
return Err(Error::new(ErrorKind::Other, "Process still running after kill attempt"))
}
info!("Killed process: {}", pid_file_path.display());
} else {
info!("Pid file {} was found, but process was not.", pid);
};
fs::remove_file(&pid_file_path)?;
info!("Deleted redundant Pid file: {}", pid_file_path.display());
Ok(())
}
pub fn log_potential_stale_process(pid_file_path: PathBuf, pid: u32) {
let mut system = System::new_all();
let pid_u32 = pid;
let pid = Pid::from_u32(pid_u32);
system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
let Some(process) = system.process(pid) else {
warn!(
"Pid file {} was not created because the process was not found.",
pid_u32
);
return;
};
match File::create(&pid_file_path) {
Ok(mut file) => {
let name = process.name().to_string_lossy();
let content = format!("{pid_u32}\n{name}\n");
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

@ -1,145 +0,0 @@
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);
}
}
}