mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-02-12 10:01:36 +00:00
fixed issues with stale processes
This commit is contained in:
parent
91bf83ea12
commit
223d288ab4
6
.github/workflows/build-and-release.yml
vendored
6
.github/workflows/build-and-release.yml
vendored
@ -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')
|
||||||
|
|||||||
@ -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.")"/>
|
||||||
|
|||||||
@ -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"] }
|
||||||
|
|||||||
189
runtime/build.rs
189
runtime/build.rs
@ -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(())
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
@ -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>> {
|
||||||
|
|||||||
94
runtime/src/stale_process_cleanup.rs
Normal file
94
runtime/src/stale_process_cleanup.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user