merge feature/67-correct-repository-branch dans develop

This commit is contained in:
2026-07-27 14:18:57 +02:00
3 changed files with 246 additions and 0 deletions

View File

@ -106,6 +106,8 @@ pub mod openai_tools;
pub mod stream; pub mod stream;
mod ticket_dto; mod ticket_dto;
pub use infrastructure::{acquire_app_data_dir_lock, AppDataDirLock, AppDataDirLockError};
use crate::mcp_endpoint::{mcp_endpoint, AppMcpRuntimeProvider, McpEndpoint}; use crate::mcp_endpoint::{mcp_endpoint, AppMcpRuntimeProvider, McpEndpoint};
use crate::openai_tools::{AppOpenAiToolInvoker, LateBoundOpenAiToolInvoker}; use crate::openai_tools::{AppOpenAiToolInvoker, LateBoundOpenAiToolInvoker};

View File

@ -0,0 +1,199 @@
//! Process-wide advisory lock for the effective IdeA app-data directory.
use std::fs::{self, File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use fs4::fs_std::FileExt;
use thiserror::Error;
const LOCK_FILE_NAME: &str = "idea.lock";
/// RAII guard holding the exclusive advisory lock on `<app_data_dir>/idea.lock`.
///
/// The lock is released when this value is dropped or the process exits.
#[derive(Debug)]
pub struct AppDataDirLock {
lock_path: PathBuf,
file: File,
}
impl AppDataDirLock {
/// Path of the lock file currently held by this guard.
#[must_use]
pub fn lock_path(&self) -> &Path {
&self.lock_path
}
}
impl Drop for AppDataDirLock {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}
/// Error returned while acquiring the app-data lock.
#[derive(Debug, Error)]
pub enum AppDataDirLockError {
/// Another process already holds the lock for this app-data directory.
#[error("app-data lock already held: {lock_path}")]
Contended {
/// Lock file path.
lock_path: PathBuf,
},
/// The lock file or its parent directory could not be accessed.
#[error("failed to access app-data lock {lock_path}: {source}")]
Io {
/// Lock file path, or the intended path when directory creation failed.
lock_path: PathBuf,
/// Underlying I/O error.
#[source]
source: io::Error,
},
}
/// Acquires an exclusive non-blocking advisory lock for `app_data_dir`.
///
/// The effective lock file is `<app_data_dir>/idea.lock`; different app-data
/// directories therefore do not block each other.
///
/// # Errors
/// [`AppDataDirLockError::Contended`] when another process already holds the
/// same lock, or [`AppDataDirLockError::Io`] for filesystem access failures.
pub fn acquire_app_data_dir_lock(
app_data_dir: impl AsRef<Path>,
) -> Result<AppDataDirLock, AppDataDirLockError> {
let app_data_dir = app_data_dir.as_ref();
let lock_path = app_data_dir.join(LOCK_FILE_NAME);
fs::create_dir_all(app_data_dir).map_err(|source| AppDataDirLockError::Io {
lock_path: lock_path.clone(),
source,
})?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&lock_path)
.map_err(|source| AppDataDirLockError::Io {
lock_path: lock_path.clone(),
source,
})?;
match file.try_lock_exclusive() {
Ok(()) => Ok(AppDataDirLock { lock_path, file }),
Err(source) if source.kind() == io::ErrorKind::WouldBlock => {
Err(AppDataDirLockError::Contended { lock_path })
}
Err(source) => Err(AppDataDirLockError::Io { lock_path, source }),
}
}
#[cfg(test)]
mod tests {
use std::env;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use super::*;
struct TempDir(PathBuf);
impl TempDir {
fn new(label: &str) -> Self {
let path =
env::temp_dir().join(format!("idea-app-lock-{label}-{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&path).unwrap();
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn contention_on_same_path_is_reported() {
let tmp = TempDir::new("same-path");
let _guard = acquire_app_data_dir_lock(tmp.path()).unwrap();
let err = acquire_app_data_dir_lock(tmp.path()).unwrap_err();
assert!(matches!(err, AppDataDirLockError::Contended { .. }));
}
#[test]
fn drop_releases_lock_and_allows_reacquire() {
let tmp = TempDir::new("reacquire");
let guard = acquire_app_data_dir_lock(tmp.path()).unwrap();
drop(guard);
let reacquired = acquire_app_data_dir_lock(tmp.path()).unwrap();
assert_eq!(reacquired.lock_path(), tmp.path().join(LOCK_FILE_NAME));
}
#[test]
fn different_app_data_dirs_can_coexist() {
let a = TempDir::new("a");
let b = TempDir::new("b");
let lock_a = acquire_app_data_dir_lock(a.path()).unwrap();
let lock_b = acquire_app_data_dir_lock(b.path()).unwrap();
assert_ne!(lock_a.lock_path(), lock_b.lock_path());
}
#[test]
fn child_process_holds_app_data_lock_until_killed() {
let Some(dir) = env::var_os("IDEA_LOCK_TEST_CHILD_DIR") else {
return;
};
let Some(ready) = env::var_os("IDEA_LOCK_TEST_CHILD_READY") else {
return;
};
let _guard = acquire_app_data_dir_lock(PathBuf::from(dir)).unwrap();
fs::write(ready, b"ready").unwrap();
thread::sleep(Duration::from_secs(30));
}
#[test]
fn second_os_process_fails_and_brutal_kill_releases_lock() {
let tmp = TempDir::new("process");
let ready = tmp.path().join("child-ready");
let exe = env::current_exe().unwrap();
let mut child = Command::new(exe)
.arg("--exact")
.arg("lock::tests::child_process_holds_app_data_lock_until_killed")
.arg("--nocapture")
.env("IDEA_LOCK_TEST_CHILD_DIR", tmp.path())
.env("IDEA_LOCK_TEST_CHILD_READY", &ready)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
while !ready.exists() && Instant::now() < deadline {
thread::sleep(Duration::from_millis(20));
}
assert!(ready.exists(), "child did not acquire the lock in time");
let err = acquire_app_data_dir_lock(tmp.path()).unwrap_err();
assert!(matches!(err, AppDataDirLockError::Contended { .. }));
child.kill().unwrap();
let status = child.wait().unwrap();
assert!(!status.success());
let reacquired = acquire_app_data_dir_lock(tmp.path()).unwrap();
assert_eq!(reacquired.lock_path(), tmp.path().join(LOCK_FILE_NAME));
}
}

View File

@ -128,6 +128,13 @@ pub fn run_from_args(args: Vec<String>) -> ExitCode {
return ExitCode::from(2); return ExitCode::from(2);
} }
}; };
let _app_data_lock = match backend::acquire_app_data_dir_lock(&config.app_data_dir) {
Ok(lock) => lock,
Err(err) => {
eprintln!("idea --serve: {}", serve_app_data_lock_message(&err));
return ExitCode::from(1);
}
};
match tokio::runtime::Builder::new_multi_thread() match tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
@ -147,6 +154,19 @@ pub fn run_from_args(args: Vec<String>) -> ExitCode {
} }
} }
fn serve_app_data_lock_message(err: &backend::AppDataDirLockError) -> String {
match err {
backend::AppDataDirLockError::Contended { lock_path } => format!(
"app-data dir is already locked at {}; another IdeA instance is using this app-data-dir",
lock_path.display()
),
backend::AppDataDirLockError::Io { lock_path, source } => format!(
"failed to access app-data lock at {}: {source}",
lock_path.display()
),
}
}
/// Configuration shared by the standalone and embedded web-server entry points. /// Configuration shared by the standalone and embedded web-server entry points.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ServerConfig { pub struct ServerConfig {
@ -5036,6 +5056,31 @@ mod tests {
} }
} }
#[test]
fn idea_serve_lock_failure_stops_before_backend_store_writes() {
let app_data_dir =
std::env::temp_dir().join(format!("idea-server-lock-{}", Uuid::new_v4()));
let web_root = create_web_root();
let _guard = backend::acquire_app_data_dir_lock(&app_data_dir).unwrap();
let code = run_from_args(vec![
"--app-data-dir".to_owned(),
app_data_dir.to_string_lossy().into_owned(),
"--web-root".to_owned(),
web_root.to_string_lossy().into_owned(),
]);
assert_eq!(code, ExitCode::from(1));
let mut entries = std::fs::read_dir(&app_data_dir)
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.collect::<Vec<_>>();
entries.sort();
assert_eq!(entries, vec!["idea.lock"]);
let _ = std::fs::remove_dir_all(app_data_dir);
let _ = std::fs::remove_dir_all(web_root);
}
#[test] #[test]
fn default_app_data_dir_uses_tauri_identifier_with_env_precedence() { fn default_app_data_dir_uses_tauri_identifier_with_env_precedence() {
let _lock = ENV_LOCK.lock().unwrap(); let _lock = ENV_LOCK.lock().unwrap();