Handle Qdrant startup failures gracefully (#683)
Some checks are pending
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Paul Koudelka 2026-03-14 12:25:58 +01:00 committed by GitHub
parent 078d48ca6f
commit 2f5a300e74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 240 additions and 66 deletions

View File

@ -5302,6 +5302,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to
-- Motivation -- Motivation
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- not available
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
@ -5323,6 +5326,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins
-- Versions -- Versions
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
-- Database
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
@ -5908,6 +5914,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = "
-- Trust all LLM providers -- Trust all LLM providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
-- Unavailable
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status"
-- Storage size -- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"

View File

@ -58,7 +58,9 @@ public partial class Information : MSGComponentBase
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
private string VersionDatabase => $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"; private string VersionDatabase => this.DatabaseClient.IsAvailable
? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"
: $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}";
private string versionPandoc = TB("Determine Pandoc version, please wait..."); private string versionPandoc = TB("Determine Pandoc version, please wait...");
private PandocInstallation pandocInstallation; private PandocInstallation pandocInstallation;

View File

@ -5304,6 +5304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwe
-- Motivation -- Motivation
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- not available
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen."
@ -5325,6 +5328,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Pandoc-Installat
-- Versions -- Versions
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen"
-- Database
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank"
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen."
@ -5910,6 +5916,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = "
-- Trust all LLM providers -- Trust all LLM providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Allen LLM-Anbietern vertrauen" UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Allen LLM-Anbietern vertrauen"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund"
-- Unavailable
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status"
-- Storage size -- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße"

View File

@ -5304,6 +5304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to
-- Motivation -- Motivation
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
-- not available
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
@ -5325,6 +5328,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins
-- Versions -- Versions
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
-- Database
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
@ -5910,6 +5916,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = "
-- Trust all LLM providers -- Trust all LLM providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
-- Unavailable
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status"
-- Storage size -- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"

View File

@ -86,37 +86,46 @@ internal sealed class Program
} }
var qdrantInfo = await rust.GetQdrantInfo(); var qdrantInfo = await rust.GetQdrantInfo();
if (qdrantInfo.Path == string.Empty) DatabaseClient databaseClient;
if (!qdrantInfo.IsAvailable)
{ {
Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'.");
return; databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason);
} }
else
if (qdrantInfo.PortHttp == 0)
{ {
Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); if (qdrantInfo.Path == string.Empty)
return; {
} Console.WriteLine("Error: Failed to get the Qdrant path from Rust.");
return;
}
if (qdrantInfo.PortGrpc == 0) if (qdrantInfo.PortHttp == 0)
{ {
Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust.");
return; return;
} }
if (qdrantInfo.Fingerprint == string.Empty) if (qdrantInfo.PortGrpc == 0)
{ {
Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust.");
return; return;
} }
if (qdrantInfo.ApiToken == string.Empty) if (qdrantInfo.Fingerprint == string.Empty)
{ {
Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust.");
return; return;
} }
var databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); if (qdrantInfo.ApiToken == string.Empty)
{
Console.WriteLine("Error: Failed to get the Qdrant API token from Rust.");
return;
}
databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
}
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(kestrelServerOptions => builder.WebHost.ConfigureKestrel(kestrelServerOptions =>

View File

@ -4,6 +4,8 @@ public abstract class DatabaseClient(string name, string path)
{ {
public string Name => name; public string Name => name;
public virtual bool IsAvailable => true;
private string Path => path; private string Path => path;
private ILogger<DatabaseClient>? logger; private ILogger<DatabaseClient>? logger;

View File

@ -0,0 +1,24 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Databases;
public sealed class NoDatabaseClient(string name, string? unavailableReason) : DatabaseClient(name, string.Empty)
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient));
public override bool IsAvailable => false;
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
{
yield return (TB("Status"), TB("Unavailable"));
if (!string.IsNullOrWhiteSpace(unavailableReason))
yield return (TB("Reason"), unavailableReason);
await Task.CompletedTask;
}
public override void Dispose()
{
}
}

View File

@ -43,8 +43,8 @@ public class QdrantClientImplementation : DatabaseClient
private async Task<string> GetVersion() private async Task<string> GetVersion()
{ {
var operation = await this.GrpcClient.HealthAsync(); var operation = await this.GrpcClient.HealthAsync();
return "v"+operation.Version; return $"v{operation.Version}";
} }
private async Task<string> GetCollectionsAmount() private async Task<string> GetCollectionsAmount()

View File

@ -5,6 +5,10 @@
/// </summary> /// </summary>
public readonly record struct QdrantInfo public readonly record struct QdrantInfo
{ {
public bool IsAvailable { get; init; }
public string? UnavailableReason { get; init; }
public string Path { get; init; } public string Path { get; init; }
public int PortHttp { get; init; } public int PortHttp { get; init; }

View File

@ -10,5 +10,6 @@
- Improved the logbook reliability by significantly reducing duplicate log entries. - Improved the logbook reliability by significantly reducing duplicate log entries.
- Improved file attachments in chats: configuration and project files such as `Dockerfile`, `Caddyfile`, `Makefile`, or `Jenkinsfile` are now included more reliably when you send them to the AI. - Improved file attachments in chats: configuration and project files such as `Dockerfile`, `Caddyfile`, `Makefile`, or `Jenkinsfile` are now included more reliably when you send them to the AI.
- Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. - Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier.
- Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize.
- Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. - Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue.
- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it.

View File

@ -107,16 +107,16 @@ pub fn start_tauri() {
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able 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();
cleanup_qdrant();
cleanup_dotnet_server();
if is_dev() { if is_dev() {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
create_startup_env_file(); create_startup_env_file();
} else { } else {
cleanup_dotnet_server();
start_dotnet_server(); start_dotnet_server();
} }
start_qdrant_server();
cleanup_qdrant();
start_qdrant_server(app.path_resolver());
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();

View File

@ -12,9 +12,10 @@ use rocket::serde::json::Json;
use rocket::serde::Serialize; use rocket::serde::Serialize;
use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::api::process::{Command, CommandChild, CommandEvent};
use crate::api_token::{APIToken}; use crate::api_token::{APIToken};
use crate::environment::DATA_DIRECTORY; use crate::environment::{is_dev, DATA_DIRECTORY};
use crate::certificate_factory::generate_certificate; use crate::certificate_factory::generate_certificate;
use std::path::PathBuf; use std::path::PathBuf;
use tauri::PathResolver;
use tempfile::{TempDir, Builder}; use tempfile::{TempDir, Builder};
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
use crate::sidecar_types::SidecarType; use crate::sidecar_types::SidecarType;
@ -38,10 +39,24 @@ static API_TOKEN: Lazy<APIToken> = Lazy::new(|| {
}); });
static TMPDIR: Lazy<Mutex<Option<TempDir>>> = Lazy::new(|| Mutex::new(None)); static TMPDIR: Lazy<Mutex<Option<TempDir>>> = Lazy::new(|| Mutex::new(None));
static QDRANT_STATUS: Lazy<Mutex<QdrantStatus>> = Lazy::new(|| Mutex::new(QdrantStatus::default()));
const PID_FILE_NAME: &str = "qdrant.pid"; const PID_FILE_NAME: &str = "qdrant.pid";
const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant; const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant;
#[derive(Default)]
struct QdrantStatus {
is_available: bool,
unavailable_reason: Option<String>,
}
fn qdrant_base_path() -> PathBuf {
let qdrant_directory = if is_dev() { "qdrant_test" } else { "qdrant" };
Path::new(DATA_DIRECTORY.get().unwrap())
.join("databases")
.join(qdrant_directory)
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct ProvideQdrantInfo { pub struct ProvideQdrantInfo {
path: String, path: String,
@ -49,34 +64,62 @@ pub struct ProvideQdrantInfo {
port_grpc: u16, port_grpc: u16,
fingerprint: String, fingerprint: String,
api_token: String, api_token: String,
is_available: bool,
unavailable_reason: Option<String>,
} }
#[get("/system/qdrant/info")] #[get("/system/qdrant/info")]
pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> { pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> {
let status = QDRANT_STATUS.lock().unwrap();
let is_available = status.is_available;
let unavailable_reason = status.unavailable_reason.clone();
Json(ProvideQdrantInfo { Json(ProvideQdrantInfo {
path: Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").to_str().unwrap().to_string(), path: if is_available {
port_http: *QDRANT_SERVER_PORT_HTTP, qdrant_base_path().to_string_lossy().to_string()
port_grpc: *QDRANT_SERVER_PORT_GRPC, } else {
fingerprint: CERTIFICATE_FINGERPRINT.get().expect("Certificate fingerprint not available").to_string(), String::new()
api_token: API_TOKEN.to_hex_text().to_string(), },
port_http: if is_available { *QDRANT_SERVER_PORT_HTTP } else { 0 },
port_grpc: if is_available { *QDRANT_SERVER_PORT_GRPC } else { 0 },
fingerprint: if is_available {
CERTIFICATE_FINGERPRINT.get().cloned().unwrap_or_default()
} else {
String::new()
},
api_token: if is_available {
API_TOKEN.to_hex_text().to_string()
} else {
String::new()
},
is_available,
unavailable_reason,
}) })
} }
/// Starts the Qdrant server in a separate process. /// Starts the Qdrant server in a separate process.
pub fn start_qdrant_server(){ pub fn start_qdrant_server(path_resolver: PathResolver){
let path = qdrant_base_path();
let base_path = DATA_DIRECTORY.get().unwrap();
let path = Path::new(base_path).join("databases").join("qdrant");
if !path.exists() { if !path.exists() {
if let Err(e) = fs::create_dir_all(&path){ 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()); error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e);
set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}"));
return;
}; };
} }
let (cert_path, key_path) =create_temp_tls_files(&path).unwrap();
let storage_path = path.join("storage").to_str().unwrap().to_string(); let (cert_path, key_path) = match create_temp_tls_files(&path) {
let snapshot_path = path.join("snapshots").to_str().unwrap().to_string(); Ok(paths) => paths,
let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string(); Err(e) => {
error!(Source="Qdrant"; "TLS files for Qdrant could not be created: {e}");
set_qdrant_unavailable(format!("TLS files for Qdrant could not be created: {e}"));
return;
}
};
let storage_path = path.join("storage").to_string_lossy().to_string();
let snapshot_path = path.join("snapshots").to_string_lossy().to_string();
let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string();
let qdrant_server_environment = HashMap::from_iter([ let qdrant_server_environment = HashMap::from_iter([
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()),
@ -84,22 +127,52 @@ pub fn start_qdrant_server(){
(String::from("QDRANT_INIT_FILE_PATH"), init_path), (String::from("QDRANT_INIT_FILE_PATH"), init_path),
(String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path),
(String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path),
(String::from("QDRANT__TLS__CERT"), cert_path.to_str().unwrap().to_string()), (String::from("QDRANT__TLS__CERT"), cert_path.to_string_lossy().to_string()),
(String::from("QDRANT__TLS__KEY"), key_path.to_str().unwrap().to_string()), (String::from("QDRANT__TLS__KEY"), key_path.to_string_lossy().to_string()),
(String::from("QDRANT__SERVICE__ENABLE_TLS"), "true".to_string()), (String::from("QDRANT__SERVICE__ENABLE_TLS"), "true".to_string()),
(String::from("QDRANT__SERVICE__API_KEY"), API_TOKEN.to_hex_text().to_string()), (String::from("QDRANT__SERVICE__API_KEY"), API_TOKEN.to_hex_text().to_string()),
]); ]);
let server_spawn_clone = QDRANT_SERVER.clone(); let server_spawn_clone = QDRANT_SERVER.clone();
let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml";
let qdrant_source_path = match path_resolver.resolve_resource(qdrant_relative_source_path) {
Some(path) => path,
None => {
let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved.");
error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant.");
set_qdrant_unavailable(reason);
return;
}
};
let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let (mut rx, child) = Command::new_sidecar("qdrant") let sidecar = match Command::new_sidecar("qdrant") {
.expect("Failed to create sidecar for Qdrant") Ok(sidecar) => sidecar,
.args(["--config-path", "resources/databases/qdrant/config.yaml"]) Err(e) => {
let reason = format!("Failed to create sidecar for Qdrant: {e}");
error!(Source = "Qdrant"; "{reason}");
set_qdrant_unavailable(reason);
return;
}
};
let (mut rx, child) = match sidecar
.args(["--config-path", qdrant_source_path_display.as_str()])
.envs(qdrant_server_environment) .envs(qdrant_server_environment)
.spawn() .spawn()
.expect("Failed to spawn Qdrant server process."); {
Ok(process) => process,
Err(e) => {
let reason = format!("Failed to spawn Qdrant server process with config path '{}': {e}", qdrant_source_path_display);
error!(Source = "Qdrant"; "{reason}");
set_qdrant_unavailable(reason);
return;
}
};
let server_pid = child.pid(); let server_pid = child.pid();
set_qdrant_available();
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_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); log_potential_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE);
@ -137,7 +210,10 @@ pub fn stop_qdrant_server() {
if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() {
let server_kill_result = server_process.kill(); let server_kill_result = server_process.kill();
match server_kill_result { match server_kill_result {
Ok(_) => warn!(Source = "Qdrant"; "Qdrant server process was stopped."), Ok(_) => {
set_qdrant_unavailable("Qdrant server was stopped.".to_string());
warn!(Source = "Qdrant"; "Qdrant server process was stopped.")
},
Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."), Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."),
} }
} else { } else {
@ -148,7 +224,7 @@ pub fn stop_qdrant_server() {
cleanup_qdrant(); cleanup_qdrant();
} }
/// Create temporary directory with TLS relevant files /// Create a temporary directory with TLS relevant files
pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<dyn Error>> { pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
let cert = generate_certificate(); let cert = generate_certificate();
@ -157,10 +233,10 @@ pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<d
let key_path = temp_dir.join("key.pem"); let key_path = temp_dir.join("key.pem");
let mut cert_file = File::create(&cert_path)?; let mut cert_file = File::create(&cert_path)?;
cert_file.write_all(&*cert.certificate)?; cert_file.write_all(&cert.certificate)?;
let mut key_file = File::create(&key_path)?; 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.");
@ -187,24 +263,35 @@ 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() { pub fn cleanup_qdrant() {
let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME); let path = qdrant_base_path();
let pid_path = path.join(PID_FILE_NAME);
if let Err(e) = kill_stale_process(pid_path, SIDECAR_TYPE) { if let Err(e) = kill_stale_process(pid_path, SIDECAR_TYPE) {
warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e);
} }
if let Err(e) = delete_old_certificates() { if let Err(e) = delete_old_certificates(path) {
warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e);
} }
} }
pub fn delete_old_certificates() -> Result<(), Box<dyn Error>> { fn set_qdrant_available() {
let dir_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant"); let mut status = QDRANT_STATUS.lock().unwrap();
status.is_available = true;
status.unavailable_reason = None;
}
if !dir_path.exists() { fn set_qdrant_unavailable(reason: String) {
let mut status = QDRANT_STATUS.lock().unwrap();
status.is_available = false;
status.unavailable_reason = Some(reason);
}
pub fn delete_old_certificates(path: PathBuf) -> Result<(), Box<dyn Error>> {
if !path.exists() {
return Ok(()); return Ok(());
} }
for entry in fs::read_dir(dir_path)? { for entry in fs::read_dir(path)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();

View File

@ -68,7 +68,7 @@
"target/databases/qdrant/qdrant" "target/databases/qdrant/qdrant"
], ],
"resources": [ "resources": [
"resources/*" "resources/**"
], ],
"macOS": { "macOS": {
"exceptionDomain": "localhost" "exceptionDomain": "localhost"