Skip to content
Open
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
2 changes: 1 addition & 1 deletion claude-code-extension/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl Extension for ClaudeCodeExtension {
"debug": true,
"websocket": {
"host": "127.0.0.1",
"portRange": [10000, 65535]
"portRange": [59790, 59799]
},
"auth": {
"generateTokens": true
Expand Down
4 changes: 2 additions & 2 deletions claude-code-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ enum Mode {
},
/// Run as standalone WebSocket server for Claude Code CLI
Websocket {
/// WebSocket server port (default: 59791)
/// WebSocket server port (default: auto-select from 59790-59799)
#[arg(long, short)]
port: Option<u16>,
},
/// Run both LSP and WebSocket servers
Hybrid {
/// WebSocket server port (default: 59791)
/// WebSocket server port (default: auto-select from 59790-59799)
#[arg(long, short)]
port: Option<u16>,
/// Worktree root path
Expand Down
67 changes: 33 additions & 34 deletions claude-code-server/src/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,48 +51,47 @@ pub async fn run_websocket_server_with_notifications(
) -> Result<()> {
info!("Starting WebSocket server...");

// Use fixed port or provided port, default to 59792
let port = port.unwrap_or(59792);

// Clean up any existing lock files for this port
cleanup_existing_lock_file(port).await?;

// Create new lock file
let auth_token = Uuid::new_v4().to_string();
create_lock_file(port, worktree.clone(), &auth_token).await?;

// Start WebSocket server with proper error handling
let addr = format!("127.0.0.1:{}", port);

// Try to bind to the port, with retry logic
let listener = match TcpListener::bind(&addr).await {
Ok(listener) => {
info!("WebSocket server listening on {}", addr);
listener
}
Err(e) => {
error!("Failed to bind to port {}: {}", port, e);
info!("Attempting to force cleanup and retry...");

// Try to cleanup and retry once
cleanup_existing_lock_file(port).await?;

// Wait a moment for the port to be released
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;

// Port range for multi-instance support (59790-59799, max 10 instances)
const PORT_RANGE_START: u16 = 59790;
const PORT_RANGE_END: u16 = 59799;

let (listener, port) = if let Some(port) = port {
// Explicit port requested - use it directly
let addr = format!("127.0.0.1:{}", port);
let listener = TcpListener::bind(&addr)
.await
.map_err(|e| anyhow!("Failed to bind to requested port {}: {}", port, e))?;
info!("WebSocket server listening on {}", addr);
(listener, port)
} else {
// No port specified - find first available port in range
let mut bound = None;
for p in PORT_RANGE_START..=PORT_RANGE_END {
let addr = format!("127.0.0.1:{}", p);
match TcpListener::bind(&addr).await {
Ok(listener) => {
info!("Successfully bound to port {} after cleanup", port);
listener
info!("WebSocket server listening on {} (auto-selected)", addr);
bound = Some((listener, p));
break;
}
Err(e2) => {
error!("Failed to bind to port {} even after cleanup: {}", port, e2);
return Err(anyhow!("Port {} is unavailable: {}", port, e2));
Err(_) => {
info!("Port {} in use, trying next...", p);
}
}
}
bound.ok_or_else(|| {
anyhow!(
"No available port in range {}-{} (max 10 instances)",
PORT_RANGE_START,
PORT_RANGE_END
)
})?
};

// Create new lock file
let auth_token = Uuid::new_v4().to_string();
create_lock_file(port, worktree.clone(), &auth_token).await?;

// Setup graceful shutdown handler
let port_for_cleanup = port;
tokio::spawn(async move {
Expand Down