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:
Mahmoud-Emad
2025-09-01 12:51:13 +03:00
parent c7724f0779
commit dde5f2f7e6
3 changed files with 134 additions and 3 deletions

123
lib/osal/core/port_check.v Normal file
View 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
}

View File

@@ -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

View File

@@ -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