From dde5f2f7e666074961f0d2586dbc496ba1b04b51 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Mon, 1 Sep 2025 12:51:13 +0300 Subject: [PATCH] feat: Add robust cross-platform port availability check - Introduce `port_check_available` function - Use platform-specific tools (`lsof`, `ss`, `netstat`) - Fallback to socket binding for port checks - Integrate port check before running `ttyd` - Simplify `tmux kill-session` error handling --- lib/osal/core/port_check.v | 123 +++++++++++++++++++++++++++++++++++ lib/osal/tmux/tmux_session.v | 9 ++- lib/osal/tmux/tmux_window.v | 5 ++ 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 lib/osal/core/port_check.v diff --git a/lib/osal/core/port_check.v b/lib/osal/core/port_check.v new file mode 100644 index 00000000..80a0dd20 --- /dev/null +++ b/lib/osal/core/port_check.v @@ -0,0 +1,123 @@ +module core + +import os +import net + +// Check if a port is available (free) on the local machine +// Returns an error if the port is already in use +pub fn port_check_available(port int) ! { + $if macos { + // On macOS, try lsof first, then fallback to netstat, then socket binding + if check_port_with_lsof(port)! { + return + } + // If lsof failed, try netstat as fallback + if check_port_with_netstat(port)! { + return + } + // If both failed, use socket binding as final fallback + check_port_with_socket_binding(port)! + } $else $if linux { + // On Linux, try ss first, then netstat, then socket binding + if check_port_with_ss(port)! { + return + } + // If ss failed, try netstat as fallback + if check_port_with_netstat(port)! { + return + } + // If both failed, use socket binding as final fallback + check_port_with_socket_binding(port)! + } $else { + // For other platforms, use socket binding directly + check_port_with_socket_binding(port)! + } + // If we reach here, the port is available +} + +// Check port availability using lsof (macOS/Linux) +fn check_port_with_lsof(port int) !bool { + // First check if lsof is available + lsof_check := os.execute('which lsof') + if lsof_check.exit_code != 0 { + return false // lsof not available, caller should try another method + } + + result := os.execute('lsof -i :${port}') + if result.exit_code == 0 { + // Port is in use, extract process info from lsof output + lines := result.output.split('\n') + if lines.len > 1 { + // Parse the first process line to get basic info + fields := lines[1].split_any(' \t').filter(it.len > 0) + if fields.len >= 2 { + process_name := fields[0] + pid := fields[1] + return error('Port ${port} is already in use by process "${process_name}" (PID: ${pid})') + } + } + return error('Port ${port} is already in use') + } + return true // Port is available +} + +// Check port availability using ss (Linux) +fn check_port_with_ss(port int) !bool { + // First check if ss is available + ss_check := os.execute('which ss') + if ss_check.exit_code != 0 { + return false // ss not available, caller should try another method + } + + result := os.execute('ss -tulpn | grep ":${port} "') + if result.exit_code == 0 { + // Port is in use, extract process info from ss output + lines := result.output.split('\n') + if lines.len > 0 && lines[0].len > 0 { + // ss output format: proto recv-q send-q local_address:port peer_address:port process + fields := lines[0].split_any(' \t').filter(it.len > 0) + if fields.len >= 6 { + protocol := fields[0] + local_addr := fields[4] + process_info := fields[6] // Usually contains "users:(("process",pid,fd))" + return error('Port ${port} is already in use by ${protocol} service at ${local_addr} (${process_info})') + } + } + return error('Port ${port} is already in use') + } + return true // Port is available +} + +// Check port availability using netstat (cross-platform fallback) +fn check_port_with_netstat(port int) !bool { + // First check if netstat is available + netstat_check := os.execute('which netstat') + if netstat_check.exit_code != 0 { + return false // netstat not available, caller should try another method + } + + // Use netstat to check for listening ports + mut result := os.Result{} + $if windows { + result = os.execute('netstat -an | findstr ":${port} "') + } $else { + result = os.execute('netstat -tuln | grep ":${port} "') + } + + if result.exit_code == 0 { + // Port is in use + return error('Port ${port} is already in use (detected by netstat)') + } + return true // Port is available +} + +// Check port availability by attempting to bind to it (most reliable fallback) +fn check_port_with_socket_binding(port int) ! { + // Try to create a TCP listener on the port + mut listener := net.listen_tcp(.ip, ':${port}') or { + return error('Port ${port} is already in use') + } + // If we successfully bound to the port, close it immediately + listener.close() or {} + // Port is available +} diff --git a/lib/osal/tmux/tmux_session.v b/lib/osal/tmux/tmux_session.v index 42aa7f73..00898873 100644 --- a/lib/osal/tmux/tmux_session.v +++ b/lib/osal/tmux/tmux_session.v @@ -305,9 +305,7 @@ pub fn (mut s Session) stop() ! { s.kill_all_processes()! // Then kill the tmux session itself - osal.execute_silent('tmux kill-session -t ${s.name}') or { - return error("Can't delete session ${s.name} - This may happen when session is not found: ${err}") - } + osal.execute_silent('tmux kill-session -t ${s.name}') or { return } } // Kill all processes in all windows and panes of this session @@ -328,6 +326,11 @@ pub fn (mut s Session) kill_all_processes() ! { // Run ttyd for this session so it can be accessed in the browser pub fn (mut s Session) run_ttyd(args TtydArgs) ! { + // Check if the port is available before starting ttyd + osal.port_check_available(args.port) or { + return error('Cannot start ttyd for session ${s.name}: ${err}') + } + target := '${s.name}' // Add -W flag for write access if editable mode is enabled diff --git a/lib/osal/tmux/tmux_window.v b/lib/osal/tmux/tmux_window.v index 8827efca..37b3be87 100644 --- a/lib/osal/tmux/tmux_window.v +++ b/lib/osal/tmux/tmux_window.v @@ -349,6 +349,11 @@ pub mut: // Run ttyd for this window so it can be accessed in the browser pub fn (mut w Window) run_ttyd(args TtydArgs) ! { + // Check if the port is available before starting ttyd + osal.port_check_available(args.port) or { + return error('Cannot start ttyd for window ${w.name}: ${err}') + } + target := '${w.session.name}:@${w.id}' // Add -W flag for write access if editable mode is enabled