larust-core 0.5.0

A Laravel-shaped web framework for Rust.
Documentation
use super::{
    AdminOutcome, ACK_HANDOFF_FAILED, ACK_HANDOFF_STARTED, BUILD_STATUS_COMMAND,
    RELOAD_ASSETS_COMMAND, RESTART_COMMAND, STOP_COMMAND,
};
use crate::lifecycle::handoff;
use std::net::TcpListener as StdTcpListener;
use std::path::PathBuf;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixListener;

fn socket_path(address: &str) -> PathBuf {
    std::env::temp_dir().join(format!("{address}.sock"))
}

pub(super) async fn run_until_command(
    address: &str,
    listener: &StdTcpListener,
    ready_timeout: Duration,
) -> AdminOutcome {
    let path = socket_path(address);
    // A stale socket file from a previous run that didn't exit cleanly
    // (killed rather than gracefully stopped) would otherwise make
    // `UnixListener::bind` fail with "address already in use" even though
    // nothing is actually listening on it anymore.
    let _ = std::fs::remove_file(&path);
    let admin_listener = UnixListener::bind(&path)
        .unwrap_or_else(|source| panic!("failed to bind admin channel at {path:?}: {source}"));

    loop {
        let Ok((stream, _)) = admin_listener.accept().await else {
            continue;
        };
        let (reader, mut writer) = tokio::io::split(stream);
        let mut lines = BufReader::new(reader).lines();
        let Ok(Some(line)) = lines.next_line().await else {
            continue;
        };
        let line = line.trim();

        if line == STOP_COMMAND {
            let _ = writer.write_all(ACK_HANDOFF_STARTED.as_bytes()).await;
            let _ = writer.write_all(b"\n").await;
            let _ = std::fs::remove_file(&path);
            return AdminOutcome::Stop;
        }

        if line == RELOAD_ASSETS_COMMAND {
            crate::dev_reload::broadcast_asset_reload();
            let _ = writer.write_all(ACK_HANDOFF_STARTED.as_bytes()).await;
            let _ = writer.write_all(b"\n").await;
            continue;
        }

        if let Some(status) = line.strip_prefix(BUILD_STATUS_COMMAND) {
            let status = status.trim();
            if !status.is_empty() {
                crate::dev_reload::broadcast_build_status(status);
            }
            let _ = writer.write_all(ACK_HANDOFF_STARTED.as_bytes()).await;
            let _ = writer.write_all(b"\n").await;
            continue;
        }

        if line != RESTART_COMMAND {
            continue;
        }

        // Resolved fresh, right here - see the matching comment in
        // `windows.rs`'s own `run_until_command` for why.
        let binary_path = match handoff::resolve_binary_path() {
            Ok(path) => path,
            Err(source) => {
                tracing::warn!(
                    error = %source,
                    "restart handoff failed before a replacement was even spawned - \
                     couldn't resolve which binary to run"
                );
                let _ = writer.write_all(ACK_HANDOFF_FAILED.as_bytes()).await;
                let _ = writer.write_all(b"\n").await;
                continue;
            }
        };

        // `false`: this is a server-to-server hop (this process spawning
        // its own replacement), not `xr dev` spawning generation 1 - see
        // `spawn_replacement_and_wait_for_ready`'s own doc comment. Unix's
        // own `supervisor::register` is a no-op today either way (see that
        // module's doc comment for the still-open Linux gap), but this
        // keeps the call site's intent consistent with `windows.rs`'s.
        match handoff::spawn_replacement_and_wait_for_ready(
            listener,
            &binary_path,
            ready_timeout,
            false,
        )
        .await
        {
            Ok(Some(child)) => {
                let _ = writer.write_all(ACK_HANDOFF_STARTED.as_bytes()).await;
                let _ = writer.write_all(b"\n").await;
                let _ = std::fs::remove_file(&path);
                return AdminOutcome::Handoff(Box::new(child));
            }
            Ok(None) => {
                // Already logged with the actual reason (timed out vs.
                // exited early) inside `spawn_replacement_and_wait_for_ready`.
                let _ = writer.write_all(ACK_HANDOFF_FAILED.as_bytes()).await;
                let _ = writer.write_all(b"\n").await;
            }
            Err(source) => {
                tracing::warn!(
                    error = %source,
                    "restart handoff failed - couldn't even spawn a replacement process"
                );
                let _ = writer.write_all(ACK_HANDOFF_FAILED.as_bytes()).await;
                let _ = writer.write_all(b"\n").await;
            }
        }
    }
}