Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ thiserror = "2"

# Logging
tracing = "0.1"
chrono = "0.4"

# Async (optional, for client)
tokio = { version = "1", features = ["rt", "net", "io-util", "time"], optional = true }
Expand Down
90 changes: 90 additions & 0 deletions logs/user_prompt_submit.json

Large diffs are not rendered by default.

122 changes: 115 additions & 7 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,25 @@ use crate::protocol::{Request, Response};
/// println!("Health: {:?}", health);
/// # Ok::<(), anyhow::Error>(())
/// ```
///
/// ## Auto-Start
///
/// Use `FgpClient::for_service()` to create a client that automatically starts
/// the daemon if it's not running:
///
/// ```rust,no_run
/// use fgp_daemon::FgpClient;
///
/// // This will auto-start the gmail daemon if needed
/// let client = FgpClient::for_service("gmail")?;
/// let response = client.call("gmail.inbox", serde_json::json!({}))?;
/// # Ok::<(), anyhow::Error>(())
/// ```
pub struct FgpClient {
socket_path: PathBuf,
timeout: Duration,
/// Service name for auto-start support
auto_start_service: Option<String>,
}

impl FgpClient {
Expand All @@ -44,6 +60,34 @@ impl FgpClient {
Ok(Self {
socket_path,
timeout: Duration::from_secs(30),
auto_start_service: None,
})
}

/// Create a client for a named service with auto-start enabled.
///
/// This is the recommended way to create a client. If the daemon is not running,
/// it will be automatically started on the first call.
///
/// # Arguments
/// * `service_name` - Name of the service (e.g., "gmail", "browser", "calendar")
///
/// # Example
///
/// ```rust,no_run
/// use fgp_daemon::FgpClient;
///
/// let client = FgpClient::for_service("gmail")?;
/// // If gmail daemon isn't running, it will be auto-started
/// let response = client.call("gmail.inbox", serde_json::json!({}))?;
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn for_service(service_name: &str) -> Result<Self> {
let socket_path = crate::lifecycle::service_socket_path(service_name);
Ok(Self {
socket_path,
timeout: Duration::from_secs(30),
auto_start_service: Some(service_name.to_string()),
})
}

Expand All @@ -53,6 +97,26 @@ impl FgpClient {
self
}

/// Enable auto-start for a specific service.
///
/// When auto-start is enabled and the daemon is not running, the client
/// will attempt to start it automatically on the first call.
///
/// # Arguments
/// * `service_name` - Name of the service to auto-start
pub fn with_auto_start(mut self, service_name: &str) -> Self {
self.auto_start_service = Some(service_name.to_string());
self
}

/// Disable auto-start.
///
/// Calls will fail immediately if the daemon is not running.
pub fn without_auto_start(mut self) -> Self {
self.auto_start_service = None;
self
}

/// Call a daemon method.
///
/// # Arguments
Expand Down Expand Up @@ -105,10 +169,35 @@ impl FgpClient {

/// Send a request and receive a response.
fn send_request(&self, request: &Request) -> Result<Response> {
// Connect to socket
let mut stream = UnixStream::connect(&self.socket_path)
.with_context(|| format!("Cannot connect to daemon at {:?}", self.socket_path))?;
// Try to connect to socket
let stream = match UnixStream::connect(&self.socket_path) {
Ok(stream) => stream,
Err(e) => {
// Connection failed - try auto-start if configured
if let Some(ref service_name) = self.auto_start_service {
tracing::info!(
"Daemon not running, auto-starting service '{}'...",
service_name
);

// Start the service
crate::lifecycle::start_service(service_name)
.with_context(|| format!("Failed to auto-start service '{}'", service_name))?;

// Retry connection
UnixStream::connect(&self.socket_path)
.with_context(|| format!("Cannot connect to daemon at {:?} after auto-start", self.socket_path))?
} else {
return Err(e).with_context(|| format!("Cannot connect to daemon at {:?}", self.socket_path));
}
}
};

self.send_request_on_stream(stream, request)
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactored send_request() method extracts stream handling into send_request_on_stream(), but this new helper method is only called from one place. Consider inlining the logic back into send_request() to reduce indirection, or document why this split is beneficial (e.g., for future testing purposes).

Copilot uses AI. Check for mistakes.
}

/// Send request on an already-connected stream.
fn send_request_on_stream(&self, mut stream: UnixStream, request: &Request) -> Result<Response> {
stream.set_read_timeout(Some(self.timeout))?;
stream.set_write_timeout(Some(self.timeout))?;

Expand All @@ -135,11 +224,15 @@ fn expand_path(path: &Path) -> Result<PathBuf> {

/// Convenience function to call a method on a daemon.
///
/// This does NOT auto-start the daemon. If the daemon is not running, the call
/// will fail. Use `call_auto_start()` if you want automatic daemon startup.
///
/// # Example
///
/// ```rust,no_run
/// use fgp_daemon::client::call;
///
/// // Fails if gmail daemon is not running
/// let response = call("gmail", "gmail.list", serde_json::json!({"limit": 5}))?;
/// # Ok::<(), anyhow::Error>(())
/// ```
Expand All @@ -149,12 +242,27 @@ pub fn call(service_name: &str, method: &str, params: serde_json::Value) -> Resu
client.call(method, params)
}

/// Call a method with auto-start enabled.
///
/// If the daemon is not running, it will be started automatically.
///
/// # Example
///
/// ```rust,no_run
/// use fgp_daemon::client::call_auto_start;
///
/// // Auto-starts gmail daemon if not running
/// let response = call_auto_start("gmail", "gmail.list", serde_json::json!({"limit": 5}))?;
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn call_auto_start(service_name: &str, method: &str, params: serde_json::Value) -> Result<Response> {
let client = FgpClient::for_service(service_name)?;
client.call(method, params)
}

/// Check if a daemon service is running.
pub fn is_running(service_name: &str) -> bool {
let socket_path = crate::lifecycle::service_socket_path(service_name);
FgpClient::new(socket_path)
.map(|c| c.is_running())
.unwrap_or(false)
crate::lifecycle::is_service_running(service_name)
}

#[cfg(test)]
Expand Down
6 changes: 5 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ pub use protocol::{Request, Response, ErrorInfo, ResponseMeta};
pub use server::FgpServer;
pub use service::FgpService;
pub use client::FgpClient;
pub use lifecycle::{daemonize, write_pid_file, cleanup_socket};
pub use lifecycle::{
daemonize, write_pid_file, cleanup_socket,
start_service, start_service_with_timeout, stop_service, is_service_running,
fgp_services_dir, service_socket_path, service_pid_path
};

#[cfg(feature = "python")]
pub use python::PythonModule;
Expand Down
Loading
Loading