Hmbown/CodeWhale · error · io::Error

control socket already live at

Error message

control socket already live at {}

What it means

prepare_socket_path refuses to take over a socket path when another live process is already answering on it. It probes the path with UnixStream::connect; if the connection succeeds, a live server owns the session's socket and this AddrInUse error is returned instead of stealing it.

Solutions

  1. Check whether another Codewhale instance for this session is running and use it instead of starting a new one
  2. Choose a different session id to get a fresh socket path
  3. If you are certain nothing is live, verify with a connect probe and remove the socket file before rebinding
  4. Handle ErrorKind::AddrInUse in the caller and surface 'session already active' to the user

Example fix

// before
let handle = bind_control_socket(&dir, &id, tx, status)?;
// after
match bind_control_socket(&dir, &id, tx, status) {
    Ok(h) => h,
    Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
        eprintln!("session {id} is already active in another instance");
        return;
    }
    Err(e) => return Err(e.into()),
}
Defensive patterns

Strategy: try-catch

Validate before calling

// probe before binding
if let Ok(_stream) = UnixStream::connect(&socket_path) {
    eprintln!("session already live; refusing to start a second instance");
    std::process::exit(0);
}

Try / catch

match bind_control_socket(&dir, &id, tx, status) {
    Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
        eprintln!("control socket already live at {}", socket_path.display());
    }
    other => other?,
}

Prevention

When it happens

Trigger: Starting a second TUI instance bound to the same session id while the first is still running; attempting bind_control_socket when the session's socket file exists and accepts connections.

Common situations: Two terminal tabs opening the same session; a stale-looking session that is actually still live on another machine/SSH session; scripts that re-launch the TUI against a running instance.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/9be08381a96870d3. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/control_socket.rs:679

    ))
}

/// Take over the socket path, or refuse when a live server already holds it.
#[cfg(unix)]
fn prepare_socket_path(path: &Path) -> io::Result<()> {
    match fs::symlink_metadata(path) {
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(error),
        Ok(metadata) => {
            if !metadata.file_type().is_socket() {
                // A plain file (or directory) in the way: not ours to keep.
                fs::remove_file(path)?;
                return Ok(());
            }
            match UnixStream::connect(path) {
                // Someone answers: a live process owns this session's socket.
                // Do not steal it (a "socket busy" refusal).
                Ok(_) => Err(io::Error::new(
                    io::ErrorKind::AddrInUse,
                    format!("control socket already live at {}", path.display()),
                )),
                // Stale: the file exists but nothing listens. Take over.
                Err(_) => {
                    fs::remove_file(path)?;
                    Ok(())
                }
            }
        }
    }
}

/// (device, inode) so an unlink never removes a file this process did not bind.
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SocketFileIdentity {
    dev: u64,

View on GitHub (pinned to 73e0f67d83)