- 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
124 lines
3.8 KiB
V
124 lines
3.8 KiB
V
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
|
|
}
|