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
This commit is contained in:
123
lib/osal/core/port_check.v
Normal file
123
lib/osal/core/port_check.v
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user