Skip to content
This repository was archived by the owner on Sep 23, 2025. It is now read-only.

Commit 1064a9b

Browse files
committed
Implement daemon subcommand for multi-window message bus
Add daemon subcommand to MCP server with Unix domain socket claiming for atomic coordination. Daemon monitors VSCode process lifecycle and prevents multiple instances per VSCode PID. Key features: - Socket claiming prevents daemon conflicts - Process monitoring with automatic cleanup - Stale socket handling for crashed daemons - Foundation for Phase 2 message bus implementation Includes research document validating Unix socket approach. See progress in issue #20.
1 parent d79b580 commit 1064a9b

File tree

4 files changed

+225
-14
lines changed

4 files changed

+225
-14
lines changed

md/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,5 @@
4242
- [Language Features](./references/lsp-overview/language-features.md) <!-- 💡: Comprehensive LSP feature catalog including navigation (go-to-definition, find references), information (hover, signature help), code intelligence (completion, actions, lens), formatting, semantic tokens, inlay hints, and diagnostics (push/pull models). Relevant for: code intelligence features, enhanced review experience, future LSP integration -->
4343
- [Implementation Guide](./references/lsp-overview/implementation-guide.md) <!-- 💡: Practical LSP server/client implementation patterns covering process isolation, message ordering, state management, error handling with exponential backoff, transport configuration (--stdio, --pipe, --socket), three-tier testing strategy, and security considerations (input validation, process isolation, path sanitization). Relevant for: robust IPC implementation, testing strategy, security best practices -->
4444
- [Message Reference](./references/lsp-overview/message-reference.md) <!-- 💡: Complete LSP message catalog with request/response pairs, notifications, $/prefixed protocol messages, capabilities exchange during initialization, document synchronization (full/incremental), workspace/window features, and proper lifecycle management (initialize → initialized → shutdown → exit). Relevant for: protocol patterns, capability negotiation, document synchronization, future LSP integration -->
45-
- [VSCode Extension Development Patterns](./references/vscode-extensions-dev-pattern.md) <!-- 💡: Comprehensive guide for VSCode extensions with separate server components covering Extension Development Host (F5) workflow, vsce packaging vs manual installation, yalc vs npm link for local dependencies, monorepo patterns with client/server/shared structure, IPC mechanisms (stdio, sockets, HTTP), setup automation with one-command experiences, and debugging configurations. Based on LSP, DAP, and MCP ecosystem patterns. Relevant for: development workflow, packaging strategy, local dependency management, project structure -->
45+
- [Unix IPC Message Bus Implementation Guide](./references/unix-message-bus-architecture.md) <!-- 💡: Comprehensive research on Unix IPC message bus patterns covering Unix domain sockets vs other mechanisms, hub-and-spoke architecture with central broker, epoll-based event handling, process lifecycle management, performance optimization through hybrid approaches, security hardening, and real-world implementations (D-Bus, Redis, nanomsg). Validates Unix sockets as superior foundation for multi-client message buses with concrete implementation patterns. Relevant for: message bus daemon design, IPC architecture decisions, multi-process communication, performance considerations -->
4646
- [Decision documents]()
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Unix IPC Message Bus Implementation Guide
2+
3+
Building a Unix IPC message bus where multiple processes connect through a shared endpoint requires careful consideration of performance, reliability, and architectural patterns. Based on comprehensive research of technical documentation, benchmarks, and production implementations, this guide provides concrete guidance for implementing such systems.
4+
5+
## Unix domain sockets emerge as the superior foundation
6+
7+
For implementing a message bus with multiple processes connecting via `/tmp/shared-endpoint`, **Unix domain sockets provide the most robust and scalable solution**. Unlike named pipes (FIFOs) which suffer from the single-reader problem and lack client identification, Unix sockets offer true multi-client support with independent connections for each process. A basic implementation creates a server socket at a filesystem path, accepts multiple client connections, and maintains a list of connected file descriptors for message broadcasting.
8+
9+
The performance characteristics strongly favor Unix sockets over other IPC mechanisms for this use case. While shared memory can achieve higher raw throughput (4-20x faster for small messages), Unix sockets provide essential features like automatic connection management, bidirectional communication, and built-in flow control that make them ideal for message bus architectures. The trade-off between raw speed and architectural cleanliness typically favors Unix sockets unless extreme performance is required.
10+
11+
## Core implementation patterns and message distribution
12+
13+
The **hub-and-spoke architecture** using a central broker process proves most effective for Unix socket-based message buses. The broker maintains connections to all clients using epoll or select for efficient I/O multiplexing, receives messages from any client, and broadcasts them to all other connected processes. This pattern scales linearly with the number of clients and provides a single point for implementing routing logic, authentication, and message transformation.
14+
15+
```c
16+
// Essential broker pattern with epoll
17+
int epfd = epoll_create1(EPOLL_CLOEXEC);
18+
struct epoll_event ev, events[MAX_EVENTS];
19+
20+
// Main event loop
21+
while (running) {
22+
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
23+
for (int i = 0; i < nfds; i++) {
24+
if (events[i].data.fd == server_fd) {
25+
accept_new_client();
26+
} else {
27+
char buffer[1024];
28+
int bytes = recv(events[i].data.fd, buffer, sizeof(buffer), 0);
29+
if (bytes > 0) {
30+
broadcast_to_all_except(buffer, bytes, events[i].data.fd);
31+
}
32+
}
33+
}
34+
}
35+
```
36+
37+
Message framing becomes critical when dealing with streaming sockets. The most reliable approach uses **length-prefixed messages** where each message begins with a fixed-size header containing the payload length. This prevents message boundary confusion and enables efficient buffer management. For maximum performance with guaranteed atomicity, messages under PIPE_BUF size (4096 bytes on Linux) can be written atomically even with multiple writers.
38+
39+
## Performance optimization through hybrid approaches
40+
41+
When extreme performance is required, a **hybrid architecture** combining multiple IPC mechanisms yields optimal results. The pattern uses Unix sockets for control messages and connection management while employing shared memory ring buffers for high-throughput data transfer. This approach can achieve 10-100x better performance than pure socket-based solutions while maintaining the architectural benefits of socket-based connection management.
42+
43+
Lock-free ring buffer implementations in shared memory can achieve over 20 million messages per second for single-producer/single-consumer scenarios. The key is careful attention to memory ordering and cache-line alignment:
44+
45+
```c
46+
struct ring_buffer {
47+
alignas(64) std::atomic<uint64_t> write_pos;
48+
alignas(64) std::atomic<uint64_t> read_pos;
49+
char data[BUFFER_SIZE];
50+
};
51+
```
52+
53+
For multi-producer scenarios, more sophisticated synchronization is required. POSIX semaphores or robust mutexes provide process-safe synchronization, with robust mutexes offering automatic cleanup when processes holding locks terminate unexpectedly.
54+
55+
## Process lifecycle and connection management
56+
57+
Proper handling of process connections and disconnections is crucial for production reliability. The message bus must detect when clients disconnect (gracefully or through crashes) and clean up resources accordingly. Unix domain sockets provide several mechanisms for this:
58+
59+
**Socket-level detection** through EPOLLHUP events or failed send operations immediately identifies disconnected clients. Setting SO_KEEPALIVE enables periodic connection verification for long-lived but idle connections. For shared memory implementations, robust mutexes (PTHREAD_MUTEX_ROBUST) automatically handle cleanup when lock-holding processes die.
60+
61+
Signal handling requires careful design to avoid race conditions. The standard pattern uses signal-safe atomic flags checked in the main event loop rather than performing cleanup directly in signal handlers:
62+
63+
```c
64+
volatile sig_atomic_t shutdown_requested = 0;
65+
66+
void signal_handler(int sig) {
67+
if (sig == SIGTERM || sig == SIGINT) {
68+
shutdown_requested = 1;
69+
}
70+
}
71+
```
72+
73+
## Concurrency, synchronization, and scalability
74+
75+
For high-concurrency scenarios, **epoll with edge-triggered mode** provides the best performance on Linux systems. This approach scales to tens of thousands of connections with O(1) event notification complexity. The event-driven architecture avoids the thread-per-connection model's memory overhead and context switching costs.
76+
77+
Synchronization between multiple writers requires careful consideration. For shared memory approaches, atomic operations and memory barriers enable lock-free implementations for specific patterns. However, most production systems benefit from the simplicity of mutex-based synchronization with proper error handling for partial operations and EINTR interruptions.
78+
79+
## Security hardening and production considerations
80+
81+
Production message bus implementations must address several security concerns. Unix domain sockets support credential passing through SO_PEERCRED, enabling authentication based on process UID/GID. File permissions on the socket path provide basic access control, though abstract namespace sockets (Linux-specific) avoid filesystem permission issues entirely.
82+
83+
Rate limiting prevents denial-of-service attacks from misbehaving clients. A simple token bucket algorithm per client connection effectively limits message rates while allowing burst traffic:
84+
85+
```c
86+
bool check_rate_limit(client_t* client) {
87+
time_t now = time(NULL);
88+
if (now > client->last_reset) {
89+
client->tokens = MAX_TOKENS;
90+
client->last_reset = now;
91+
}
92+
if (client->tokens > 0) {
93+
client->tokens--;
94+
return true;
95+
}
96+
return false;
97+
}
98+
```
99+
100+
## Real-world implementations and architectural choices
101+
102+
Production systems demonstrate various architectural trade-offs. **D-Bus**, the Linux desktop standard, uses Unix domain sockets with a central daemon providing message routing, service activation, and security policy enforcement. Its hub-and-spoke architecture handles system-wide and per-user session buses effectively but incurs ~2.5x overhead compared to direct IPC.
103+
104+
**Redis** configured with Unix sockets for local communication provides a pragmatic pub/sub message bus with persistence options and rich data structures. While not as performant as custom solutions, Redis offers battle-tested reliability and extensive language bindings.
105+
106+
For embedded systems or performance-critical applications, **nanomsg/nng** provides a socket-like API with multiple messaging patterns including bus topology. It abstracts the underlying IPC mechanism while providing zero-copy message passing and automatic reconnection.
107+
108+
## Conclusion
109+
110+
Implementing a Unix IPC message bus requires balancing performance, reliability, and complexity. **Unix domain sockets provide the best foundation for most use cases**, offering natural multi-client support, connection management, and sufficient performance for typical messaging workloads. When extreme performance is required, hybrid approaches combining sockets for control with shared memory for data transfer can achieve orders of magnitude better throughput.
111+
112+
The key to a successful implementation lies in careful attention to process lifecycle management, proper error handling for partial operations, and appropriate synchronization mechanisms. Whether building a simple pub/sub system or a complex service bus, the patterns and techniques outlined here provide a solid foundation for robust inter-process communication on Unix systems.

server/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ tracing-appender = "0.2"
4646
# Command line argument parsing
4747
clap = { version = "4.0", features = ["derive"] }
4848

49+
# Unix system calls for process monitoring
50+
nix = { version = "0.27", features = ["signal", "process"] }
51+
4952
[dev-dependencies]
5053
tokio-test = { workspace = true }
5154
# Add client feature for testing

server/src/main.rs

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,24 @@ use dialectic_mcp_server::{pid_discovery, DialecticServer};
1818
#[command(about = "Dialectic MCP Server for VSCode integration")]
1919
struct Args {
2020
/// Run PID discovery probe and exit (for testing)
21-
#[arg(long)]
21+
#[arg(long, global = true)]
2222
probe: bool,
2323

2424
/// Enable development logging to /tmp/dialectic-mcp-server.log
25-
#[arg(long)]
25+
#[arg(long, global = true)]
2626
dev_log: bool,
27+
28+
#[command(subcommand)]
29+
command: Option<Command>,
30+
}
31+
32+
#[derive(Parser)]
33+
enum Command {
34+
/// Run as message bus daemon for multi-window support
35+
Daemon {
36+
/// VSCode process ID to monitor
37+
vscode_pid: u32,
38+
},
2739
}
2840

2941
#[tokio::main]
@@ -69,26 +81,110 @@ async fn main() -> Result<()> {
6981
return Ok(());
7082
}
7183

72-
info!("Starting Dialectic MCP Server (Rust)");
84+
match args.command {
85+
Some(Command::Daemon { vscode_pid }) => {
86+
info!("🚀 DAEMON MODE - Starting message bus daemon for VSCode PID {}", vscode_pid);
87+
run_daemon(vscode_pid).await?;
88+
}
89+
None => {
90+
info!("Starting Dialectic MCP Server (Rust)");
7391

74-
// Create our server instance
75-
let server = DialecticServer::new().await?;
92+
// Create our server instance
93+
let server = DialecticServer::new().await?;
7694

77-
// Start the MCP server with stdio transport
78-
let service = server.serve(stdio()).await.inspect_err(|e| {
79-
error!("MCP server error: {:?}", e);
80-
})?;
95+
// Start the MCP server with stdio transport
96+
let service = server.serve(stdio()).await.inspect_err(|e| {
97+
error!("MCP server error: {:?}", e);
98+
})?;
8199

82-
info!("Dialectic MCP Server is ready and listening");
100+
info!("Dialectic MCP Server is ready and listening");
83101

84-
// Wait for the service to complete
85-
service.waiting().await?;
102+
// Wait for the service to complete
103+
service.waiting().await?;
86104

87-
info!("Dialectic MCP Server shutting down");
105+
info!("Dialectic MCP Server shutting down");
106+
}
107+
}
88108
std::mem::drop(flush_guard);
89109
Ok(())
90110
}
91111

112+
/// Run the message bus daemon for multi-window support
113+
async fn run_daemon(vscode_pid: u32) -> Result<()> {
114+
use std::os::unix::net::UnixListener;
115+
use std::path::Path;
116+
117+
let socket_path = format!("/tmp/dialectic-vscode-{}.sock", vscode_pid);
118+
info!("Attempting to claim socket: {}", socket_path);
119+
120+
// Try to bind to the socket first - this is our "claim" operation
121+
let _listener = match UnixListener::bind(&socket_path) {
122+
Ok(listener) => {
123+
info!("✅ Successfully claimed socket: {}", socket_path);
124+
listener
125+
}
126+
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
127+
error!("❌ Failed to claim socket {}: {}", socket_path, e);
128+
error!("Another daemon is already running for VSCode PID {}", vscode_pid);
129+
return Err(e.into());
130+
}
131+
Err(e) => {
132+
// Other error - maybe stale socket file, try to remove and retry once
133+
if Path::new(&socket_path).exists() {
134+
std::fs::remove_file(&socket_path)?;
135+
info!("Removed stale socket file, retrying bind");
136+
137+
// Retry binding once
138+
match UnixListener::bind(&socket_path) {
139+
Ok(listener) => {
140+
info!("✅ Successfully claimed socket after cleanup: {}", socket_path);
141+
listener
142+
}
143+
Err(e) => {
144+
error!("❌ Failed to claim socket {} even after cleanup: {}", socket_path, e);
145+
return Err(e.into());
146+
}
147+
}
148+
} else {
149+
error!("❌ Failed to claim socket {}: {}", socket_path, e);
150+
return Err(e.into());
151+
}
152+
}
153+
};
154+
155+
info!("🚀 Message bus daemon started for VSCode PID {}", vscode_pid);
156+
info!("📡 Listening on socket: {}", socket_path);
157+
158+
// TODO: Implement the actual message bus loop
159+
// For now, just keep the socket claimed and monitor the VSCode process
160+
loop {
161+
// Check if VSCode process is still alive
162+
match nix::sys::signal::kill(nix::unistd::Pid::from_raw(vscode_pid as i32), None) {
163+
Ok(_) => {
164+
// Process exists, continue
165+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
166+
}
167+
Err(nix::errno::Errno::ESRCH) => {
168+
info!("VSCode process {} has died, shutting down daemon", vscode_pid);
169+
break;
170+
}
171+
Err(e) => {
172+
error!("Error checking VSCode process {}: {}", vscode_pid, e);
173+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
174+
}
175+
}
176+
}
177+
178+
// Clean up socket file on exit
179+
if Path::new(&socket_path).exists() {
180+
std::fs::remove_file(&socket_path)?;
181+
info!("🧹 Cleaned up socket file: {}", socket_path);
182+
}
183+
184+
info!("🛑 Daemon shutdown complete");
185+
Ok(())
186+
}
187+
92188
/// Run PID discovery probe for testing
93189
async fn run_pid_probe() -> Result<()> {
94190
use std::process;

0 commit comments

Comments
 (0)