app-instance-detector 1.0.0

A Rust library for detecting 1 or more instances of an app are running locally, that doesn't use lockfiles.
Documentation
// ==== ./src/lib.rs ====
/*!
 * This file is part of App Instance Detector, an Autonomo AI, FZCO, Project.
 * autonomo-file-copy is a component of Autonomo AI Programming Agent.
 *
 * Copyright © 2025 Autonomo AI, FZCO
 * Author: Theodore R. Smith <theodore.smith@autonomo.codes>
 *   GPG Fingerprint: 6CAC F838 454C 8912 8AA2  26DB 89DC D8F1 3BB9 33B3
 *   https://www.phpexperts.pro/
 *   https://www.autonomo.codes/
 *   https://github.com/AutonomoDev/autonomo
 *
 * This file is proprietary.
 * All rights are reserved.
 */

use std::{
    io::{Read, Write},
    net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream},
    thread,
    time::Duration,
};

/// Finds all running instances by checking ports sequentially and sending a customizable handshake request.
/// Returns a vector of tuples, where each tuple contains the port and the
/// string response received from the running instance.
///
/// The `request_message` is the byte sequence sent to each potential instance.
/// The `timeout` applies to both connecting and reading the response.
pub fn find_all_instances(
    base_port: u16,
    max_search: u16,
    timeout: Duration,
    request_message: &[u8],
) -> Vec<(u16, String)> {
    let mut instances = Vec::new();

    for port in base_port..(base_port + max_search) {
        // Attempt a handshake. If it succeeds, add it to our list.
        // This is more direct than checking is_port_in_use first.
        if let Ok(response) = send_handshake_request(port, timeout, request_message) {
            instances.push((port, response));
        }
    }

    instances
}

/// Finds the next available port starting from `base_port` by checking sequentially.
///
/// It iterates through ports from `base_port` up to `base_port + max_search - 1`.
/// Returns the first port in the search range that is not in use.
/// If no port is found within the specified range, it returns `base_port` as a fallback.
pub fn find_next_available_port(base_port: u16, max_search: u16) -> u16 {
    for port in base_port..(base_port + max_search) {
        if !is_port_in_use(port) {
            return port;
        }
    }
    // Fallback if no port is found
    base_port
}

/// Starts a background handshake server on the given port.
///
/// The server listens for any incoming TCP connection. For each connection, it attempts
/// to read some data (the "handshake request"). It then responds with a string
/// generated by the `response_generator` closure.
///
/// This function spawns a new thread and returns immediately.
pub fn start_handshake_server<F>(port: u16, response_generator: F)
where
    F: Fn() -> String + Send + Sync + 'static + Clone,
{
    thread::spawn(move || {
        let listener = match TcpListener::bind((Ipv4Addr::LOCALHOST, port)) {
            Ok(listener) => listener,
            Err(e) => {
                eprintln!("[!] Failed to bind handshake server on port {}: {}", port, e);
                return;
            }
        };
        eprintln!("[+] Handshake server listening on port {}", port);

        for stream in listener.incoming() {
            match stream {
                Ok(mut stream) => {
                    let local_generator = response_generator.clone();
                    thread::spawn(move || {
                        let mut buffer = [0u8; 1024];
                        let peer_addr = stream.peer_addr()
                            .map(|a| a.to_string())
                            .unwrap_or_else(|_| "unknown".to_string());
                        
                        // Handle the handshake.
                        match stream.read(&mut buffer) {
                            Ok(_) => {
                                // We don't care how many bytes were read, just that a request was made.
                                let response_str = local_generator();
                                if let Err(e) = stream.write_all(response_str.as_bytes()) {
                                    eprintln!("[!] Error sending handshake response to {}: {}", peer_addr, e);
                                }
                            }
                            Err(e) => {
                                eprintln!("[!] Error reading handshake request from {}: {}", peer_addr, e);
                            }
                        }
                    });
                }
                Err(e) => {
                    eprintln!("[!] Handshake server connection error on port {}: {}", port, e);
                }
            }
        }
        eprintln!("[-] Handshake server on port {} stopped.", port);
    });
}

/// Checks if a port is in use by attempting to connect to it.
/// This function uses a short, fixed timeout.
pub fn is_port_in_use(port: u16) -> bool {
    let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port);
    TcpStream::connect_timeout(&addr, Duration::from_millis(100)).is_ok()
}

/// Sends a customizable handshake request to a given port and returns the response.
pub fn send_handshake_request(
    port: u16,
    timeout: Duration,
    request_message: &[u8],
) -> anyhow::Result<String> {
    let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port);
    let mut stream = TcpStream::connect_timeout(&addr, timeout)?;

    stream.set_read_timeout(Some(timeout))?;
    stream.write_all(request_message)?;

    let mut buffer = [0u8; 1024];
    let n = stream.read(&mut buffer)?;
    let response = String::from_utf8_lossy(&buffer[..n]).to_string();

    Ok(response)
}