ripht-php-sapi 0.1.0-rc.9

Ripht PHP SAPI - A PHP SAPI written in Rust to expose safe and convenient APIs to encourage additional Rust tooling development for PHP
Documentation
#![allow(dead_code)]

use super::backend::Backend;
use super::env::{frankenphp_bin, next_port, scripts_dir};
use super::protocol::Method;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
use std::time::Duration;

pub struct FrankenPhpBackend {
    process: Child,
    port: u16,
}

impl FrankenPhpBackend {
    pub fn start() -> Option<Self> {
        let bin = frankenphp_bin()?;
        let port = next_port();

        let process = Command::new(&bin)
            .arg("php-server")
            .arg("--listen")
            .arg(format!("127.0.0.1:{}", port))
            .arg("--root")
            .arg(scripts_dir())
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .ok()?;

        for _ in 0..50 {
            if TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() {
                std::thread::sleep(Duration::from_millis(50));
                return Some(Self { process, port });
            }
            std::thread::sleep(Duration::from_millis(100));
        }

        eprintln!(
            "FrankenPhpBackend: timed out waiting for server on port {}",
            port
        );
        None
    }

    fn http_execute(
        &self,
        script: &str,
        method: Method,
        body: Option<&[u8]>,
    ) -> Option<Vec<u8>> {
        let mut stream =
            TcpStream::connect(format!("127.0.0.1:{}", self.port)).ok()?;
        stream
            .set_read_timeout(Some(Duration::from_secs(5)))
            .ok()?;
        stream
            .set_write_timeout(Some(Duration::from_secs(5)))
            .ok()?;

        let content_len = body
            .map(|b| b.len())
            .unwrap_or(0);

        let request = if body.is_some() {
            format!(
                "{} /{} HTTP/1.1\r\n\
                 Host: 127.0.0.1:{}\r\n\
                 Content-Type: application/json\r\n\
                 Content-Length: {}\r\n\
                 Connection: close\r\n\r\n",
                method.as_str(),
                script,
                self.port,
                content_len
            )
        } else {
            format!(
                "{} /{} HTTP/1.1\r\n\
                 Host: 127.0.0.1:{}\r\n\
                 Connection: close\r\n\r\n",
                method.as_str(),
                script,
                self.port
            )
        };

        stream
            .write_all(request.as_bytes())
            .ok()?;

        if let Some(b) = body {
            stream.write_all(b).ok()?;
        }

        stream.flush().ok()?;

        let mut response = Vec::new();
        stream
            .read_to_end(&mut response)
            .ok()?;

        if let Some(pos) = response
            .windows(4)
            .position(|w| w == b"\r\n\r\n")
        {
            Some(response[pos + 4..].to_vec())
        } else {
            Some(response)
        }
    }
}

impl Backend for FrankenPhpBackend {
    fn name(&self) -> &'static str {
        "frankenphp"
    }

    fn execute(
        &mut self,
        script: &str,
        method: Method,
        body: Option<&[u8]>,
    ) -> Option<Vec<u8>> {
        self.http_execute(script, method, body)
    }
}

impl Drop for FrankenPhpBackend {
    fn drop(&mut self) {
        let _ = self.process.kill();
        let _ = self.process.wait();
    }
}