-
Notifications
You must be signed in to change notification settings - Fork 0
feat(daemon): add auto-start support for daemon services #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Add start_service() and stop_service() to lifecycle module - Add is_service_running() helper function - Add FgpClient::for_service() constructor with auto-start - Add with_auto_start() and without_auto_start() builder methods - Update client::call() to use auto-start by default - Add call_no_auto_start() for explicit opt-out Auto-start detects when a daemon is not running and automatically starts it on first connection attempt. This improves DX by removing the need to manually start daemons before making calls. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds auto-start functionality for FGP daemon services, allowing clients to automatically spawn stopped daemons on connection failure. This improves developer experience by eliminating the need to manually start daemons before making service calls.
Changes:
- Added
start_service()andstop_service()functions to the lifecycle module for programmatic daemon management - Added
FgpClient::for_service()constructor with auto-start enabled by default, and builder methodswith_auto_start()/without_auto_start()for explicit control - Updated
client::call()to use auto-start by default, with newcall_no_auto_start()function for legacy behavior
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| src/lifecycle.rs | Adds service management functions (start_service, stop_service, is_service_running) with manifest parsing and daemon spawning logic |
| src/lib.rs | Exports new lifecycle functions for public API |
| src/client.rs | Adds auto-start support to FgpClient with new constructor, builder methods, and refactored connection logic |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let _child = Command::new(&entrypoint_path) | ||
| .current_dir(&service_dir) | ||
| .spawn() | ||
| .context("Failed to start daemon")?; |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spawned child process is not configured to detach from the parent. Without setting stdin, stdout, and stderr to null, the child process may inherit file descriptors and could be affected by the parent process termination. Use std::process::Stdio::null() for all standard streams to properly daemonize.
| tracing::info!("Starting service '{}'...", service_name); | ||
|
|
||
| // Start as background process | ||
| let _child = Command::new(&entrypoint_path) |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spawned child process handle is immediately dropped. If the daemon entrypoint fails to properly daemonize itself (via the daemonize() function), the process will become a zombie when the parent exits. Consider either waiting briefly to verify the process is stable, or document that daemon entrypoints must call daemonize() to properly detach.
| } | ||
| }; | ||
|
|
||
| self.send_request_on_stream(stream, request) |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
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).
| return Ok(()); | ||
| } | ||
| } | ||
| std::thread::sleep(Duration::from_millis(50)); |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The polling loop sleeps for 50ms between socket connection attempts. This means even if the daemon starts quickly, there's an unnecessary 0-50ms delay. Consider using a smaller initial sleep (e.g., 10ms) or implementing exponential backoff to balance responsiveness and CPU usage.
| // Wait a moment for graceful shutdown | ||
| std::thread::sleep(Duration::from_millis(500)); |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded 500ms wait after sending SIGTERM assumes the daemon will gracefully shut down within this time. Consider polling the process status instead to detect when it actually terminates, or make this timeout configurable. For daemons with cleanup work, 500ms may be insufficient.
| // Wait a moment for graceful shutdown | |
| std::thread::sleep(Duration::from_millis(500)); | |
| // Wait for graceful shutdown, polling until the process exits or a timeout elapses | |
| let timeout = Duration::from_secs(5); | |
| let start = Instant::now(); | |
| while is_process_running(pid) && start.elapsed() < timeout { | |
| std::thread::sleep(Duration::from_millis(100)); | |
| } |
| pub fn call(service_name: &str, method: &str, params: serde_json::Value) -> Result<Response> { | ||
| let client = FgpClient::for_service(service_name)?; | ||
| client.call(method, params) | ||
| } |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing call() to auto-start by default is a breaking behavioral change. Existing code that relies on fast failure when a daemon is not running will now experience startup delays. Consider adding this as a new function (e.g., call_with_auto_start()) and keeping call() unchanged, or bumping the major version if this is intentional.
| let _child = Command::new(&entrypoint_path) | ||
| .current_dir(&service_dir) | ||
| .spawn() | ||
| .context("Failed to start daemon")?; |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The entrypoint path is read from the manifest.json file and executed without validation. While there's an existence check, there's no verification of file permissions or ownership. A compromised or malicious manifest could potentially execute arbitrary code. Consider validating that the entrypoint is executable and optionally checking file ownership matches expectations.
| if std::os::unix::net::UnixStream::connect(&socket_path).is_ok() { | ||
| tracing::debug!("Service '{}' is already running", service_name); | ||
| return Ok(()); |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a socket connection succeeds, the function returns Ok without closing the connection, potentially leaving an open socket. The UnixStream is dropped immediately which closes it, but this isn't explicit. While this works, consider explicitly documenting this behavior or using let _stream = to make the immediate drop clearer.
- Make call() NOT auto-start by default (preserves existing behavior) - Add call_auto_start() for explicit auto-start functionality - Add validate_entrypoint() security check for executable permissions - Warn on world-writable entrypoints Addresses PR review comments about breaking changes and security. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
start_service()andstop_service()to lifecycle module for programmatic daemon managementFgpClient::for_service()constructor that auto-starts daemons on connection failurewith_auto_start()andwithout_auto_start()builder methods for explicit controlclient::call()to use auto-start by defaultWhy
Auto-start improves developer experience by removing the need to manually start daemons before making calls. When a client tries to connect to a stopped daemon, it automatically starts it.
Test plan
cargo testpassesfgp callcommand--no-auto-startflag fails appropriately🤖 Generated with Claude Code