fix: use a lock to ensure only need to run tunnel just in case multiple go… (#5885)
This commit is contained in:
Generated
+1
@@ -2871,6 +2871,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
|
"fs2",
|
||||||
"futures",
|
"futures",
|
||||||
"goose",
|
"goose",
|
||||||
"goose-mcp",
|
"goose-mcp",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ url = "2.5.7"
|
|||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
socket2 = "0.6.1"
|
socket2 = "0.6.1"
|
||||||
|
fs2 = "0.4.3"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = { version = "0.55.0" }
|
winreg = { version = "0.55.0" }
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ pub mod lapstone;
|
|||||||
mod lapstone_test;
|
mod lapstone_test;
|
||||||
|
|
||||||
use crate::configuration::Settings;
|
use crate::configuration::Settings;
|
||||||
use goose::config::Config;
|
use fs2::FileExt as _;
|
||||||
|
use goose::config::{paths::Paths, Config};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs::{File, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{mpsc, RwLock};
|
use tokio::sync::{mpsc, RwLock};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
@@ -15,6 +18,53 @@ fn get_server_port() -> anyhow::Result<u16> {
|
|||||||
Ok(settings.port)
|
Ok(settings.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_lock_path() -> std::path::PathBuf {
|
||||||
|
Paths::config_dir().join("tunnel.lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_acquire_tunnel_lock() -> anyhow::Result<File> {
|
||||||
|
let lock_path = get_lock_path();
|
||||||
|
|
||||||
|
if let Some(parent) = lock_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(&lock_path)?;
|
||||||
|
|
||||||
|
file.try_lock_exclusive()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Another goose instance is already running the tunnel"))?;
|
||||||
|
|
||||||
|
writeln!(file, "{}", std::process::id())?;
|
||||||
|
file.sync_all()?;
|
||||||
|
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_tunnel_locked_by_another() -> bool {
|
||||||
|
let lock_path = get_lock_path();
|
||||||
|
|
||||||
|
let file = match OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(&lock_path)
|
||||||
|
{
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if file.try_lock_exclusive().is_err() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock released when file is dropped
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum TunnelState {
|
pub enum TunnelState {
|
||||||
@@ -40,6 +90,7 @@ pub struct TunnelManager {
|
|||||||
lapstone_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
lapstone_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
||||||
restart_tx: Arc<RwLock<Option<mpsc::Sender<()>>>>,
|
restart_tx: Arc<RwLock<Option<mpsc::Sender<()>>>>,
|
||||||
watchdog_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
watchdog_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
||||||
|
lock_file: Arc<std::sync::Mutex<Option<File>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TunnelManager {
|
impl Default for TunnelManager {
|
||||||
@@ -56,6 +107,7 @@ impl TunnelManager {
|
|||||||
lapstone_handle: Arc::new(RwLock::new(None)),
|
lapstone_handle: Arc::new(RwLock::new(None)),
|
||||||
restart_tx: Arc::new(RwLock::new(None)),
|
restart_tx: Arc::new(RwLock::new(None)),
|
||||||
watchdog_handle: Arc::new(RwLock::new(None)),
|
watchdog_handle: Arc::new(RwLock::new(None)),
|
||||||
|
lock_file: Arc::new(std::sync::Mutex::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +130,20 @@ impl TunnelManager {
|
|||||||
let state = self.state.read().await.clone();
|
let state = self.state.read().await.clone();
|
||||||
|
|
||||||
if auto_start && state == TunnelState::Idle {
|
if auto_start && state == TunnelState::Idle {
|
||||||
|
if is_tunnel_locked_by_another() {
|
||||||
|
tracing::info!(
|
||||||
|
"Tunnel already running on another goose instance, skipping auto-start"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tracing::info!("Auto-starting tunnel");
|
tracing::info!("Auto-starting tunnel");
|
||||||
match self.start().await {
|
match self.start().await {
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
tracing::info!("Tunnel auto-started successfully: {:?}", info.url);
|
tracing::info!("Tunnel auto-started successfully: {:?}", info.url);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to auto-start tunnel: {}", e);
|
tracing::info!("Tunnel auto-start skipped: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,12 +176,20 @@ impl TunnelManager {
|
|||||||
tunnel_info.state = state;
|
tunnel_info.state = state;
|
||||||
tunnel_info
|
tunnel_info
|
||||||
}
|
}
|
||||||
None => TunnelInfo {
|
None => {
|
||||||
state,
|
let effective_state = if state == TunnelState::Idle && is_tunnel_locked_by_another()
|
||||||
url: String::new(),
|
{
|
||||||
hostname: String::new(),
|
TunnelState::Running
|
||||||
secret: String::new(),
|
} else {
|
||||||
},
|
state
|
||||||
|
};
|
||||||
|
TunnelInfo {
|
||||||
|
state: effective_state,
|
||||||
|
url: String::new(),
|
||||||
|
hostname: String::new(),
|
||||||
|
secret: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +249,10 @@ impl TunnelManager {
|
|||||||
if *state != TunnelState::Idle {
|
if *state != TunnelState::Idle {
|
||||||
anyhow::bail!("Tunnel is already running or starting");
|
anyhow::bail!("Tunnel is already running or starting");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lock = try_acquire_tunnel_lock()?;
|
||||||
|
*self.lock_file.lock().unwrap() = Some(lock);
|
||||||
|
|
||||||
*state = TunnelState::Starting;
|
*state = TunnelState::Starting;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
@@ -230,6 +301,7 @@ impl TunnelManager {
|
|||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
self.release_lock();
|
||||||
*self.state.write().await = TunnelState::Error;
|
*self.state.write().await = TunnelState::Error;
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
@@ -243,6 +315,14 @@ impl TunnelManager {
|
|||||||
lapstone_handle: self.lapstone_handle.clone(),
|
lapstone_handle: self.lapstone_handle.clone(),
|
||||||
restart_tx: self.restart_tx.clone(),
|
restart_tx: self.restart_tx.clone(),
|
||||||
watchdog_handle: self.watchdog_handle.clone(),
|
watchdog_handle: self.watchdog_handle.clone(),
|
||||||
|
lock_file: self.lock_file.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_lock(&self) {
|
||||||
|
if let Ok(mut guard) = self.lock_file.lock() {
|
||||||
|
// Dropping the file releases the lock
|
||||||
|
guard.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +335,8 @@ impl TunnelManager {
|
|||||||
|
|
||||||
lapstone::stop(self.lapstone_handle.clone()).await;
|
lapstone::stop(self.lapstone_handle.clone()).await;
|
||||||
|
|
||||||
|
self.release_lock();
|
||||||
|
|
||||||
*self.state.write().await = TunnelState::Idle;
|
*self.state.write().await = TunnelState::Idle;
|
||||||
*self.info.write().await = None;
|
*self.info.write().await = None;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user