Check whether a local port is available for forwarding. Uses a two-pronged check: 1. Attempts to bind ` : ` — catches same-family conflicts. 2. Runs `lsof -i : -sTCP:LISTEN` — catches cross-family conflicts (e.g. an IPv6 wildcard listener blocking a port the IPv4 bind test would miss). If the port is already in use the error message includes an actionable hint: - If an exis
(spec: &ForwardSpec)
| 624 | /// - Otherwise, show the `lsof` output and suggest `kill` to terminate the |
| 625 | /// owning process. |
| 626 | pub fn check_port_available(spec: &ForwardSpec) -> Result<()> { |
| 627 | let port = spec.port; |
| 628 | |
| 629 | // Fast path: try binding on the requested address. If this fails, the |
| 630 | // port is definitely taken on this address family. |
| 631 | let bind_ok = TcpListener::bind((spec.bind_addr.as_str(), port)).is_ok(); |
| 632 | |
| 633 | // Also ask the OS whether *any* process is listening on this port, |
| 634 | // regardless of address family. This catches situations where e.g. a |
| 635 | // server binds [::]:8080 but our IPv4 bind test succeeds. |
| 636 | let lsof_output = lsof_listeners(port); |
| 637 | let lsof_occupied = lsof_output.is_some(); |
| 638 | |
| 639 | if bind_ok && !lsof_occupied { |
| 640 | return Ok(()); |
| 641 | } |
| 642 | |
| 643 | // Port is occupied. Check if it belongs to a tracked openshell forward. |
| 644 | if let Ok(forwards) = list_forwards() |
| 645 | && let Some(fwd) = forwards |
| 646 | .iter() |
| 647 | .find(|f| f.port == port && f.validated_alive) |
| 648 | { |
| 649 | return Err(miette::miette!( |
| 650 | "Port {port} is already forwarded to sandbox '{}'.\n\ |
| 651 | Stop it with: openshell forward stop {port} {}", |
| 652 | fwd.sandbox_name, |
| 653 | fwd.sandbox_name, |
| 654 | )); |
| 655 | } |
| 656 | |
| 657 | // Build a helpful error with lsof details when available. |
| 658 | if let Some(output) = lsof_output { |
| 659 | return Err(miette::miette!( |
| 660 | "Port {port} is already in use by another process.\n\n\ |
| 661 | {output}\n\n\ |
| 662 | To free the port, find the PID above and run:\n \ |
| 663 | kill <PID>\n\n\ |
| 664 | Or find it yourself with:\n \ |
| 665 | lsof -i :{port} -sTCP:LISTEN", |
| 666 | )); |
| 667 | } |
| 668 | |
| 669 | Err(miette::miette!( |
| 670 | "Port {port} is already in use by another process.\n\ |
| 671 | Find it with: lsof -i :{port} -sTCP:LISTEN\n\ |
| 672 | Then terminate it with: kill <PID>", |
| 673 | )) |
| 674 | } |
| 675 | |
| 676 | /// Run `lsof` to check for any process listening on `port`. |
| 677 | /// |