Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ jobs:
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: none
tools: composer:v2
coverage: none
ini-values: phar.readonly=Off
Expand Down Expand Up @@ -67,10 +66,19 @@ jobs:
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: none
tools: composer:v2
coverage: none

- name: cache composer
uses: actions/cache@v4
with:
path: |
vendor
~/.cache/composer
key: ${{ runner.os }}-php-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-composer-

- name: composer install
run: composer install --no-progress --ansi

Expand All @@ -95,8 +103,25 @@ jobs:
tools: composer:v2
coverage: none

- name: cache composer
uses: actions/cache@v4
with:
path: |
vendor
~/.cache/composer
key: ${{ runner.os }}-php-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-composer-

- name: composer validate
run: composer validate --strict --no-check-publish

- name: install.sh syntax
run: bash -n scripts/install.sh
run: bash -n scripts/install.sh

all-checks:
name: all checks passed
needs: [build-extension, php-quality, metadata]
runs-on: ubuntu-latest
steps:
- run: echo "All required checks passed."
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Release 页会同时挂出符合 PIE 命名规范的 ZIP 包,以及 `liblychee
原始文件。只需:

```bash
wget https://github.com/watsonhaw/lychee-worker/releases/latest/download/liblychee_worker-$(uname)-x86_64.so
wget https://github.com/watsonhaw5566/lychee-worker/releases/latest/download/liblychee_worker-$(uname)-x86_64.so
cp liblychee_worker-*.so "$(php-config --extension-dir)/lychee_worker.so"
echo 'extension=lychee_worker.so' >> "$(php --ini | awk -F': ' '/Loaded Configuration File/ {print $2; exit}')"
php -m | grep lychee_worker
Expand Down
2 changes: 1 addition & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ fn main() {
println!("cargo:rustc-link-arg=-undefined");
println!("cargo:rustc-link-arg=dynamic_lookup");
}
}
}
16 changes: 0 additions & 16 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "watsonhaw/lychee-worker",
"type": "library",
"extension-name": "lychee_worker",
"description": "Rust-powered high-performance runtime — prefork HTTP/WebSocket server with hot-reload",
"license": "MIT",
"keywords": [
Expand Down Expand Up @@ -55,21 +54,6 @@
}
}
},
"php-ext": {
"extension-name": "lychee_worker",
"build-system": "cargo",
"configure-options": [
{
"name": "enable-lychee-worker",
"type": "bool",
"default": true,
"description": "Enable the lychee_worker extension (Rust-powered runtime)"
}
],
"php-versions": ["8.3"],
"operating-systems": ["Linux", "macOS"],
"architectures": ["x86_64", "arm64"]
},
"scripts": {
"build": "cargo build --release",
"install-ext": "bash scripts/install.sh",
Expand Down
4 changes: 2 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 48 additions & 33 deletions rust/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ pub fn probe_port(host: &str, port: u16) -> Result<(), String> {
let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
.map_err(|e: std::net::AddrParseError| e.to_string())?;
let domain = if addr.is_ipv6() { Domain::IPV6 } else { Domain::IPV4 };
let domain = if addr.is_ipv6() {
Domain::IPV6
} else {
Domain::IPV4
};
let socket = Socket::new(domain, Type::STREAM, Some(socket2::Protocol::TCP))
.map_err(|e| format!("socket: {}", e))?;
let _ = socket.set_reuse_address(true);
Expand All @@ -31,7 +35,11 @@ pub fn probe_port(host: &str, port: u16) -> Result<(), String> {
/// 以 SO_REUSEADDR + SO_REUSEPORT 的方式绑定端口,允许多个
/// HTTP 子进程共享监听端口,从而由内核在进程间做负载均衡。
fn bind_with_reuse(addr: SocketAddr) -> std::io::Result<TcpListener> {
let domain = if addr.is_ipv6() { Domain::IPV6 } else { Domain::IPV4 };
let domain = if addr.is_ipv6() {
Domain::IPV6
} else {
Domain::IPV4
};
let socket = Socket::new(domain, Type::STREAM, Some(socket2::Protocol::TCP))?;
socket.set_reuse_address(true)?;
socket.set_reuse_port(true)?;
Expand All @@ -50,11 +58,12 @@ pub async fn serve<'a>(
ws_message_handler: Option<&'a ZendCallable<'a>>,
ws_close_handler: Option<&'a ZendCallable<'a>>,
) -> std::io::Result<()> {
let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
.map_err(|e: std::net::AddrParseError| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string())
})?;
let addr: SocketAddr =
format!("{}:{}", host, port)
.parse()
.map_err(|e: std::net::AddrParseError| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string())
})?;
let listener = bind_with_reuse(addr)?;

// 将回调引用泄露为 'static,以便在 tokio spawn_local 的任务中使用
Expand All @@ -65,36 +74,41 @@ pub async fn serve<'a>(
let leaked_close = leak_callable(ws_close_handler);

let local = tokio::task::LocalSet::new();
local.run_until(async move {
loop {
match listener.accept().await {
Ok((stream, _remote)) => {
tokio::task::spawn_local(async move {
let _ = handle_connection(
stream,
leaked_http,
leaked_open,
leaked_msg,
leaked_close,
).await;
});
}
Err(e) => {
eprintln!("[lychee-worker] accept error: {}", e);
local
.run_until(async move {
loop {
match listener.accept().await {
Ok((stream, _remote)) => {
tokio::task::spawn_local(async move {
let _ = handle_connection(
stream,
leaked_http,
leaked_open,
leaked_msg,
leaked_close,
)
.await;
});
}
Err(e) => {
eprintln!("[lychee-worker] accept error: {}", e);
}
}
}
}
}).await;
})
.await;

Ok(())
}

fn leak_callable<'a>(
cb: Option<&'a ZendCallable<'a>>,
) -> &'static Option<&'static ZendCallable<'static>> {
let boxed: Box<Option<&'static ZendCallable<'static>>> = Box::new(
unsafe { std::mem::transmute::<Option<&'a ZendCallable<'a>>, Option<&'static ZendCallable<'static>>>(cb) },
);
let boxed: Box<Option<&'static ZendCallable<'static>>> = Box::new(unsafe {
std::mem::transmute::<Option<&'a ZendCallable<'a>>, Option<&'static ZendCallable<'static>>>(
cb,
)
});
Box::leak(boxed)
}

Expand Down Expand Up @@ -220,9 +234,10 @@ fn extract_headers_text(header_bytes: &[u8]) -> String {
lines.remove(0);
}
// 去掉最后一个空元素(由末尾 \r\n\r\n 产生)
while lines.last().map_or(false, |l| l.trim().is_empty()) {
lines.pop();
if lines.is_empty() {
while let Some(last) = lines.last() {
if last.trim().is_empty() {
lines.pop();
} else {
break;
}
}
Expand All @@ -236,7 +251,7 @@ fn parse_method_path(bytes: &[u8]) -> (String, String) {
.map(|l| String::from_utf8_lossy(l).to_string())
.unwrap_or_default();
let parts: Vec<&str> = first_line.split_whitespace().collect();
let method = parts.get(0).copied().unwrap_or("GET").to_string();
let method = parts.first().copied().unwrap_or("GET").to_string();
let path = parts.get(1).copied().unwrap_or("/").to_string();
(method, path)
}
}
15 changes: 10 additions & 5 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

use ext_php_rs::builders::ModuleBuilder;
use ext_php_rs::exception::PhpException;
use ext_php_rs::types::{ZendCallable, Zval};
use ext_php_rs::wrap_function;
use ext_php_rs::php_function;
use ext_php_rs::php_module;
use ext_php_rs::types::{ZendCallable, Zval};
use ext_php_rs::wrap_function;

mod http;
mod runtime;
Expand All @@ -36,6 +36,7 @@ fn parse_watch_list(s: &str) -> Vec<String> {
}

#[php_function]
#[allow(clippy::too_many_arguments)]
pub fn lychee_worker_start(
host: String,
port: i64,
Expand All @@ -58,8 +59,12 @@ pub fn lychee_worker_start(
let ws_close_callable = ws_close_handler.and_then(|z| ZendCallable::new(z).ok());

if !(0..=65535).contains(&port) {
let _ = PhpException::new("invalid port".to_string(), 0, ext_php_rs::zend::ce::exception())
.throw();
let _ = PhpException::new(
"invalid port".to_string(),
0,
ext_php_rs::zend::ce::exception(),
)
.throw();
return false;
}
if worker_num < 1 {
Expand Down Expand Up @@ -191,4 +196,4 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
.function(wrap_function!(lychee_worker_stats))
.function(wrap_function!(lychee_worker_trigger_reload))
.function(wrap_function!(lychee_worker_stop))
}
}
11 changes: 4 additions & 7 deletions rust/src/php_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,10 @@ pub fn call_on_http(
let path_s = path.to_string();
let headers_s = headers.to_string();
let body_s = body.to_string();
match h.try_call(vec![&method_s, &path_s, &headers_s, &body_s]) {
Ok(result) => {
if let Some(s) = result.string() {
return s;
}
if let Ok(result) = h.try_call(vec![&method_s, &path_s, &headers_s, &body_s]) {
if let Some(s) = result.string() {
return s;
}
Err(_) => {}
}
}
format!(
Expand Down Expand Up @@ -58,4 +55,4 @@ pub fn call_on_ws_close(handler: Option<&ZendCallable<'_>>, conn_id: &str) {
/// 返回 PHP 运行时统计信息(键值对)。
pub fn stats() -> Vec<(String, i64)> {
crate::stats::snapshot().into_iter().collect()
}
}
Loading
Loading