mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
feat: add support for PHP timeouts on Linux (#128)
This commit is contained in:
92
Dockerfile
92
Dockerfile
@@ -1,9 +1,88 @@
|
||||
FROM php:8.2-zts-bullseye AS builder
|
||||
FROM php:8.2-zts-bullseye AS php-base
|
||||
|
||||
# Note that this image is based on the official PHP image, once https://github.com/php/php-src/pull/10141 is merged, this stage can be removed
|
||||
|
||||
RUN rm -Rf /usr/local/include/php/ /usr/local/lib/libphp.* /usr/local/lib/php/ /usr/local/php/
|
||||
|
||||
ENV PHPIZE_DEPS \
|
||||
autoconf \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
pkg-config \
|
||||
re2c
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
$PHPIZE_DEPS \
|
||||
libargon2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libreadline-dev \
|
||||
libsodium-dev \
|
||||
libsqlite3-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
bison \
|
||||
git \
|
||||
&& \
|
||||
apt-get clean
|
||||
|
||||
RUN git clone --depth=1 --single-branch --branch=PHP-8.2 https://github.com/php/php-src.git && \
|
||||
cd php-src && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
./buildconf && \
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself)
|
||||
--enable-mysqlnd \
|
||||
# make sure invalid --configure-flags are fatal errors instead of just warnings
|
||||
--enable-option-checking=fatal \
|
||||
# https://github.com/docker-library/php/issues/439
|
||||
--with-mhash \
|
||||
# https://github.com/docker-library/php/issues/822
|
||||
--with-pic \
|
||||
# --enable-ftp is included here because ftp_ssl_connect() needs ftp to be compiled statically (see https://github.com/docker-library/php/issues/236)
|
||||
--enable-ftp \
|
||||
# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195)
|
||||
--enable-mbstring \
|
||||
# https://wiki.php.net/rfc/argon2_password_hash
|
||||
--with-password-argon2 \
|
||||
# https://wiki.php.net/rfc/libsodium
|
||||
--with-sodium=shared \
|
||||
# always build against system sqlite3 (https://github.com/php/php-src/commit/6083a387a81dbbd66d6316a3a12a63f06d5f7109)
|
||||
--with-pdo-sqlite=/usr \
|
||||
--with-sqlite3=/usr \
|
||||
--with-curl \
|
||||
--with-iconv \
|
||||
--with-openssl \
|
||||
--with-readline \
|
||||
--with-zlib \
|
||||
# https://github.com/bwoebi/phpdbg-docs/issues/1#issuecomment-163872806 ("phpdbg is primarily a CLI debugger, and is not suitable for debugging an fpm stack.")
|
||||
--disable-phpdbg \
|
||||
--with-config-file-path="$PHP_INI_DIR" \
|
||||
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d" && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
rm -Rf php-src/ && \
|
||||
echo "Creating src archive for building extensions\n" && \
|
||||
tar -c -f /usr/src/php.tar.xz -J /php-src/ && \
|
||||
ldconfig && \
|
||||
php --version
|
||||
|
||||
FROM php-base AS builder
|
||||
|
||||
COPY --from=golang:1.19-bullseye /usr/local/go/bin/go /usr/local/bin/go
|
||||
COPY --from=golang:1.19-bullseye /usr/local/go /usr/local/go
|
||||
|
||||
# This is required to link the frankenPHP binary to the PHP binary
|
||||
# This is required to link the FrankenPHP binary to the PHP binary
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
libargon2-dev \
|
||||
@@ -35,7 +114,7 @@ COPY internal internal
|
||||
COPY testdata testdata
|
||||
|
||||
# todo: automate this?
|
||||
# see https://github.com/docker-library/php/blob/master/8.2-rc/bullseye/zts/Dockerfile#L57-L59 for php values
|
||||
# see https://github.com/docker-library/php/blob/master/8.2/bullseye/zts/Dockerfile#L57-L59 for PHP values
|
||||
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS=$PHP_CFLAGS CGO_CPPFLAGS=$PHP_CPPFLAGS
|
||||
|
||||
RUN cd caddy/frankenphp && \
|
||||
@@ -57,6 +136,13 @@ RUN echo '<?php phpinfo();' > /app/public/index.php
|
||||
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
COPY --from=builder /etc/Caddyfile /etc/Caddyfile
|
||||
|
||||
COPY --from=php-base /usr/local/include/php/ /usr/local/include/php
|
||||
COPY --from=php-base /usr/local/lib/libphp.* /usr/local/lib
|
||||
COPY --from=php-base /usr/local/lib/php/ /usr/local/lib/php
|
||||
COPY --from=php-base /usr/local/php/ /usr/local/php
|
||||
COPY --from=php-base /usr/local/bin/ /usr/local/bin
|
||||
COPY --from=php-base /usr/src /usr/src
|
||||
|
||||
RUN sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint
|
||||
|
||||
CMD [ "--config", "/etc/Caddyfile" ]
|
||||
|
||||
@@ -1,4 +1,84 @@
|
||||
FROM php:8.2-zts-alpine3.17 AS builder
|
||||
FROM php:8.2-zts-alpine3.17 AS php-base
|
||||
|
||||
# Note that this image is based on the official PHP image, once https://github.com/php/php-src/pull/10141 is merged, this stage can be removed
|
||||
|
||||
RUN rm -Rf /usr/local/include/php/ /usr/local/lib/libphp.* /usr/local/lib/php/ /usr/local/php/
|
||||
|
||||
ENV PHPIZE_DEPS \
|
||||
autoconf \
|
||||
dpkg-dev dpkg \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
pkgconf \
|
||||
re2c
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
$PHPIZE_DEPS \
|
||||
argon2-dev \
|
||||
coreutils \
|
||||
curl-dev \
|
||||
readline-dev \
|
||||
libsodium-dev \
|
||||
sqlite-dev \
|
||||
openssl-dev \
|
||||
libxml2-dev \
|
||||
gnu-libiconv-dev \
|
||||
linux-headers \
|
||||
oniguruma-dev \
|
||||
bison \
|
||||
git
|
||||
|
||||
RUN git clone --depth=1 --single-branch --branch=PHP-8.2 https://github.com/php/php-src.git
|
||||
|
||||
WORKDIR /php-src/
|
||||
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
RUN ./buildconf
|
||||
RUN ./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself)
|
||||
--enable-mysqlnd \
|
||||
# make sure invalid --configure-flags are fatal errors instead of just warnings
|
||||
--enable-option-checking=fatal \
|
||||
# https://github.com/docker-library/php/issues/439
|
||||
--with-mhash \
|
||||
# https://github.com/docker-library/php/issues/822
|
||||
--with-pic \
|
||||
# --enable-ftp is included here because ftp_ssl_connect() needs ftp to be compiled statically (see https://github.com/docker-library/php/issues/236)
|
||||
--enable-ftp \
|
||||
# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195)
|
||||
--enable-mbstring \
|
||||
# https://wiki.php.net/rfc/argon2_password_hash
|
||||
--with-password-argon2 \
|
||||
# https://wiki.php.net/rfc/libsodium
|
||||
--with-sodium=shared \
|
||||
# always build against system sqlite3 (https://github.com/php/php-src/commit/6083a387a81dbbd66d6316a3a12a63f06d5f7109)
|
||||
--with-pdo-sqlite=/usr \
|
||||
--with-sqlite3=/usr \
|
||||
--with-curl \
|
||||
--with-iconv \
|
||||
--with-openssl \
|
||||
--with-readline \
|
||||
--with-zlib \
|
||||
# https://github.com/bwoebi/phpdbg-docs/issues/1#issuecomment-163872806 ("phpdbg is primarily a CLI debugger, and is not suitable for debugging an fpm stack.")
|
||||
--disable-phpdbg \
|
||||
--with-config-file-path="$PHP_INI_DIR" \
|
||||
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d"
|
||||
RUN make -j$(nproc)
|
||||
RUN make install
|
||||
RUN rm -Rf /php-src/
|
||||
RUN echo "Creating src archive for building extensions\n"
|
||||
RUN tar -c -f /usr/src/php.tar.xz -J /php-src/
|
||||
#RUN ldconfig
|
||||
RUN php --version
|
||||
|
||||
FROM php-base AS builder
|
||||
|
||||
COPY --from=golang:1.19-alpine3.17 /usr/local/go/bin/go /usr/local/bin/go
|
||||
COPY --from=golang:1.19-alpine3.17 /usr/local/go /usr/local/go
|
||||
@@ -56,6 +136,13 @@ RUN echo '<?php phpinfo();' > /app/public/index.php
|
||||
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
COPY --from=builder /etc/Caddyfile /etc/Caddyfile
|
||||
|
||||
COPY --from=php-base /usr/local/include/php/ /usr/local/include/php
|
||||
COPY --from=php-base /usr/local/lib/libphp.* /usr/local/lib
|
||||
COPY --from=php-base /usr/local/lib/php/ /usr/local/lib/php
|
||||
COPY --from=php-base /usr/local/php/ /usr/local/php
|
||||
COPY --from=php-base /usr/local/bin/ /usr/local/bin
|
||||
COPY --from=php-base /usr/src /usr/src
|
||||
|
||||
RUN sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint
|
||||
|
||||
CMD [ "--config", "/etc/Caddyfile" ]
|
||||
|
||||
@@ -30,7 +30,8 @@ RUN apt-get update && \
|
||||
gdb \
|
||||
valgrind \
|
||||
neovim \
|
||||
zsh && \
|
||||
zsh \
|
||||
libtool-bin && \
|
||||
echo 'set auto-load safe-path /' > /root/.gdbinit && \
|
||||
echo '* soft core unlimited' >> /etc/security/limits.conf \
|
||||
&& \
|
||||
@@ -44,6 +45,7 @@ RUN git clone --branch=PHP-8.2 https://github.com/php/php-src.git && \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
--enable-debug && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
|
||||
@@ -20,7 +20,8 @@ Then, configure PHP for your platform:
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers
|
||||
```
|
||||
|
||||
### Mac
|
||||
|
||||
142
frankenphp.c
142
frankenphp.c
@@ -2,16 +2,18 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <php_config.h>
|
||||
#include <php.h>
|
||||
#include <SAPI.h>
|
||||
#include <ext/standard/head.h>
|
||||
#include <php_main.h>
|
||||
#include <php_variables.h>
|
||||
#include <php_output.h>
|
||||
#include <SAPI.h>
|
||||
#include <Zend/zend_alloc.h>
|
||||
#include <Zend/zend_types.h>
|
||||
#include <Zend/zend_exceptions.h>
|
||||
#include <Zend/zend_interfaces.h>
|
||||
#include <ext/standard/head.h>
|
||||
#include <ext/spl/spl_exceptions.h>
|
||||
|
||||
#include "C-Thread-Pool/thpool.h"
|
||||
#include "C-Thread-Pool/thpool.c"
|
||||
@@ -23,10 +25,12 @@
|
||||
ZEND_TSRMLS_CACHE_DEFINE()
|
||||
#endif
|
||||
|
||||
/* Timeouts are currently fundamentally broken with ZTS: https://bugs.php.net/bug.php?id=79464 */
|
||||
/* Timeouts are currently fundamentally broken with ZTS except on Linux: https://bugs.php.net/bug.php?id=79464 */
|
||||
#ifndef ZEND_MAX_EXECUTION_TIMERS
|
||||
static const char HARDCODED_INI[] =
|
||||
"max_execution_time=0\n"
|
||||
"max_input_time=-1\n\0";
|
||||
#endif
|
||||
|
||||
static const char *MODULES_TO_RELOAD[] = {
|
||||
"filter",
|
||||
@@ -34,8 +38,8 @@ static const char *MODULES_TO_RELOAD[] = {
|
||||
NULL
|
||||
};
|
||||
|
||||
frankenphp_php_version frankenphp_version() {
|
||||
return (frankenphp_php_version){
|
||||
frankenphp_version frankenphp_get_version() {
|
||||
return (frankenphp_version){
|
||||
PHP_MAJOR_VERSION,
|
||||
PHP_MINOR_VERSION,
|
||||
PHP_RELEASE_VERSION,
|
||||
@@ -45,26 +49,31 @@ frankenphp_php_version frankenphp_version() {
|
||||
};
|
||||
}
|
||||
|
||||
int frankenphp_check_version() {
|
||||
#ifndef ZTS
|
||||
return -1;
|
||||
frankenphp_config frankenphp_get_config() {
|
||||
return (frankenphp_config){
|
||||
frankenphp_get_version(),
|
||||
#ifdef ZTS
|
||||
true,
|
||||
#else
|
||||
false,
|
||||
#endif
|
||||
|
||||
if (PHP_VERSION_ID < 80200) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
#ifdef ZEND_SIGNALS
|
||||
return -3;
|
||||
true,
|
||||
#else
|
||||
false,
|
||||
#endif
|
||||
|
||||
return SUCCESS;
|
||||
#ifdef ZEND_MAX_EXECUTION_TIMERS
|
||||
true,
|
||||
#else
|
||||
false,
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
typedef struct frankenphp_server_context {
|
||||
bool worker;
|
||||
uintptr_t current_request;
|
||||
uintptr_t main_request; /* Only available during worker initialization */
|
||||
uintptr_t main_request;
|
||||
bool worker_ready;
|
||||
char *cookie_data;
|
||||
bool finished;
|
||||
} frankenphp_server_context;
|
||||
@@ -82,17 +91,12 @@ static void frankenphp_request_reset() {
|
||||
}
|
||||
|
||||
/* Adapted from php_request_shutdown */
|
||||
static void frankenphp_worker_request_shutdown(uintptr_t current_request) {
|
||||
static void frankenphp_worker_request_shutdown() {
|
||||
/* Flush all output buffers */
|
||||
zend_try {
|
||||
php_output_end_all();
|
||||
} zend_end_try();
|
||||
|
||||
/* Reset max_execution_time (no longer executing php code after response sent) */
|
||||
/*zend_try {
|
||||
zend_unset_timeout();
|
||||
} zend_end_try();*/
|
||||
|
||||
// TODO: store the list of modules to reload in a global module variable
|
||||
const char **module_name;
|
||||
zend_module_entry *module;
|
||||
@@ -116,10 +120,7 @@ static void frankenphp_worker_request_shutdown(uintptr_t current_request) {
|
||||
sapi_deactivate();
|
||||
} zend_end_try();
|
||||
|
||||
if (current_request != 0) go_frankenphp_worker_handle_request_end(current_request, true);
|
||||
|
||||
zend_set_memory_limit(PG(memory_limit));
|
||||
|
||||
}
|
||||
|
||||
/* Adapted from php_request_startup() */
|
||||
@@ -189,23 +190,25 @@ static int frankenphp_worker_request_startup() {
|
||||
|
||||
PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
|
||||
if (zend_parse_parameters_none() == FAILURE) {
|
||||
RETURN_THROWS();
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if(!ctx->finished) {
|
||||
php_output_end_all();
|
||||
php_header();
|
||||
if (ctx->finished) {
|
||||
RETURN_FALSE;
|
||||
}
|
||||
|
||||
go_frankenphp_worker_handle_request_end(ctx->current_request, false);
|
||||
ctx->finished = true;
|
||||
php_output_end_all();
|
||||
php_header();
|
||||
|
||||
RETURN_TRUE;
|
||||
}
|
||||
if (ctx->current_request != 0) {
|
||||
go_frankenphp_finish_request(ctx->main_request, ctx->current_request, false);
|
||||
}
|
||||
|
||||
RETURN_FALSE;
|
||||
ctx->finished = true;
|
||||
|
||||
RETURN_TRUE;
|
||||
} /* }}} */
|
||||
|
||||
PHP_FUNCTION(frankenphp_handle_request) {
|
||||
@@ -218,24 +221,40 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
uintptr_t previous_request = ctx->current_request;
|
||||
if (ctx->main_request) {
|
||||
/* Clean the first dummy request created to initialize the worker */
|
||||
frankenphp_worker_request_shutdown(0);
|
||||
if (ctx->main_request == 0) {
|
||||
// not a worker, throw an error
|
||||
zend_throw_exception(spl_ce_RuntimeException, "frankenphp_handle_request() called while not in worker mode", 0);
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
previous_request = ctx->main_request;
|
||||
if (!ctx->worker_ready) {
|
||||
/* Clean the first dummy request created to initialize the worker */
|
||||
frankenphp_worker_request_shutdown();
|
||||
|
||||
ctx->worker_ready = true;
|
||||
|
||||
/* Mark the worker as ready to handle requests */
|
||||
go_frankenphp_worker_ready();
|
||||
}
|
||||
|
||||
uintptr_t next_request = go_frankenphp_worker_handle_request_start(previous_request);
|
||||
#ifdef ZEND_MAX_EXECUTION_TIMERS
|
||||
// Disable timeouts while waiting for a request to handle
|
||||
zend_unset_timeout();
|
||||
#endif
|
||||
|
||||
uintptr_t request = go_frankenphp_worker_handle_request_start(ctx->main_request);
|
||||
if (
|
||||
frankenphp_worker_request_startup() == FAILURE
|
||||
/* Shutting down */
|
||||
|| !next_request
|
||||
|| !request
|
||||
) RETURN_FALSE;
|
||||
|
||||
#ifdef ZEND_MAX_EXECUTION_TIMERS
|
||||
// Reset default timeout
|
||||
// TODO: add support for max_input_time
|
||||
zend_set_timeout(INI_INT("max_execution_time"), 0);
|
||||
#endif
|
||||
|
||||
/* Call the PHP func */
|
||||
zval retval = {0};
|
||||
fci.size = sizeof fci;
|
||||
@@ -245,10 +264,12 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
}
|
||||
|
||||
/* If an exception occured, print the message to the client before closing the connection */
|
||||
if (EG(exception))
|
||||
if (EG(exception)) {
|
||||
zend_exception_error(EG(exception), E_ERROR);
|
||||
}
|
||||
|
||||
frankenphp_worker_request_shutdown(next_request);
|
||||
frankenphp_worker_request_shutdown();
|
||||
go_frankenphp_finish_request(ctx->main_request, request, true);
|
||||
|
||||
RETURN_TRUE;
|
||||
}
|
||||
@@ -319,7 +340,7 @@ uintptr_t frankenphp_request_shutdown()
|
||||
{
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (ctx->worker && ctx->current_request) {
|
||||
if (ctx->main_request && ctx->current_request) {
|
||||
frankenphp_request_reset();
|
||||
}
|
||||
|
||||
@@ -358,32 +379,28 @@ int frankenphp_update_server_context(
|
||||
|
||||
if (create) {
|
||||
#ifdef ZTS
|
||||
/* initial resource fetch */
|
||||
(void)ts_resource(0);
|
||||
/* initial resource fetch */
|
||||
(void)ts_resource(0);
|
||||
# ifdef PHP_WIN32
|
||||
ZEND_TSRMLS_CACHE_UPDATE();
|
||||
ZEND_TSRMLS_CACHE_UPDATE();
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/* todo: use a pool */
|
||||
ctx = (frankenphp_server_context *) calloc(1, sizeof(frankenphp_server_context));
|
||||
if (ctx == NULL) return FAILURE;
|
||||
/* todo: use a pool */
|
||||
ctx = (frankenphp_server_context *) calloc(1, sizeof(frankenphp_server_context));
|
||||
if (ctx == NULL) return FAILURE;
|
||||
|
||||
ctx->worker = false;
|
||||
ctx->current_request = 0;
|
||||
ctx->main_request = 0;
|
||||
ctx->cookie_data = NULL;
|
||||
ctx->finished = false;
|
||||
ctx->cookie_data = NULL;
|
||||
ctx->finished = false;
|
||||
|
||||
SG(server_context) = ctx;
|
||||
} else
|
||||
SG(server_context) = ctx;
|
||||
} else {
|
||||
ctx = (frankenphp_server_context *) SG(server_context);
|
||||
}
|
||||
|
||||
ctx->main_request = main_request;
|
||||
ctx->current_request = current_request;
|
||||
|
||||
if (ctx->main_request) ctx->worker = true;
|
||||
|
||||
SG(request_info).auth_password = auth_password;
|
||||
SG(request_info).auth_user = auth_user;
|
||||
SG(request_info).request_method = request_method;
|
||||
@@ -541,6 +558,7 @@ sapi_module_struct frankenphp_sapi_module = {
|
||||
|
||||
static void *manager_thread(void *arg) {
|
||||
#ifdef ZTS
|
||||
// TODO: use tsrm_startup() directly as we now the number of expected threads
|
||||
php_tsrm_startup();
|
||||
/*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/
|
||||
# ifdef PHP_WIN32
|
||||
@@ -550,8 +568,10 @@ static void *manager_thread(void *arg) {
|
||||
|
||||
sapi_startup(&frankenphp_sapi_module);
|
||||
|
||||
#ifndef ZEND_MAX_EXECUTION_TIMERS
|
||||
frankenphp_sapi_module.ini_entries = malloc(sizeof(HARDCODED_INI));
|
||||
memcpy(frankenphp_sapi_module.ini_entries, HARDCODED_INI, sizeof(HARDCODED_INI));
|
||||
#endif
|
||||
|
||||
frankenphp_sapi_module.startup(&frankenphp_sapi_module);
|
||||
|
||||
|
||||
@@ -130,8 +130,9 @@ type FrankenPHPContext struct {
|
||||
// Whether the request is already closed by us
|
||||
closed sync.Once
|
||||
|
||||
responseWriter http.ResponseWriter
|
||||
done chan interface{}
|
||||
responseWriter http.ResponseWriter
|
||||
done chan interface{}
|
||||
currentWorkerRequest cgo.Handle
|
||||
}
|
||||
|
||||
func clientHasClosed(r *http.Request) bool {
|
||||
@@ -174,9 +175,16 @@ type PHPVersion struct {
|
||||
VersionID int
|
||||
}
|
||||
|
||||
type PHPConfig struct {
|
||||
Version PHPVersion
|
||||
ZTS bool
|
||||
ZendSignals bool
|
||||
ZendMaxExecutionTimers bool
|
||||
}
|
||||
|
||||
// Version returns infos about the PHP version.
|
||||
func Version() PHPVersion {
|
||||
cVersion := C.frankenphp_version()
|
||||
cVersion := C.frankenphp_get_version()
|
||||
|
||||
return PHPVersion{
|
||||
int(cVersion.major_version),
|
||||
@@ -188,6 +196,17 @@ func Version() PHPVersion {
|
||||
}
|
||||
}
|
||||
|
||||
func Config() PHPConfig {
|
||||
cConfig := C.frankenphp_get_config()
|
||||
|
||||
return PHPConfig{
|
||||
Version: Version(),
|
||||
ZTS: bool(cConfig.zts),
|
||||
ZendSignals: bool(cConfig.zend_signals),
|
||||
ZendMaxExecutionTimers: bool(cConfig.zend_max_execution_timers),
|
||||
}
|
||||
}
|
||||
|
||||
// Init starts the PHP runtime and the configured workers.
|
||||
func Init(options ...Option) error {
|
||||
if requestChan != nil {
|
||||
@@ -238,18 +257,19 @@ func Init(options ...Option) error {
|
||||
return NotEnoughThreads
|
||||
}
|
||||
|
||||
switch C.frankenphp_check_version() {
|
||||
case -1:
|
||||
if opt.numThreads != 1 {
|
||||
opt.numThreads = 1
|
||||
logger.Warn(`ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
|
||||
}
|
||||
config := Config()
|
||||
|
||||
case -2:
|
||||
if config.Version.MajorVersion < 8 || config.Version.MinorVersion < 2 {
|
||||
return InvalidPHPVersionError
|
||||
}
|
||||
|
||||
case -3:
|
||||
return ZendSignalsError
|
||||
if config.ZTS {
|
||||
if !config.ZendMaxExecutionTimers && runtime.GOOS == "linux" {
|
||||
logger.Warn(`Zend Timer is not enabled, "--enable-zend-timer" configuration option or timeouts (e.g. "max_execution_time") will not work as expected`)
|
||||
}
|
||||
} else {
|
||||
opt.numThreads = 1
|
||||
logger.Warn(`ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
|
||||
}
|
||||
|
||||
shutdownWG.Add(1)
|
||||
@@ -293,7 +313,7 @@ func getLogger() *zap.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
func updateServerContext(request *http.Request, create bool) error {
|
||||
func updateServerContext(request *http.Request, create bool, mrh C.uintptr_t) error {
|
||||
fc, ok := FromContext(request.Context())
|
||||
if !ok {
|
||||
return InvalidRequestError
|
||||
@@ -333,9 +353,9 @@ func updateServerContext(request *http.Request, create bool) error {
|
||||
|
||||
cRequestUri := C.CString(request.URL.RequestURI())
|
||||
|
||||
var rh, mwrh cgo.Handle
|
||||
var rh cgo.Handle
|
||||
if fc.responseWriter == nil {
|
||||
mwrh = cgo.NewHandle(request)
|
||||
mrh = C.uintptr_t(cgo.NewHandle(request))
|
||||
} else {
|
||||
rh = cgo.NewHandle(request)
|
||||
}
|
||||
@@ -343,7 +363,7 @@ func updateServerContext(request *http.Request, create bool) error {
|
||||
ret := C.frankenphp_update_server_context(
|
||||
C.bool(create),
|
||||
C.uintptr_t(rh),
|
||||
C.uintptr_t(mwrh),
|
||||
mrh,
|
||||
|
||||
cMethod,
|
||||
cQueryString,
|
||||
@@ -425,7 +445,7 @@ func go_execute_script(rh unsafe.Pointer) {
|
||||
}
|
||||
defer maybeCloseContext(fc)
|
||||
|
||||
if err := updateServerContext(request, true); err != nil {
|
||||
if err := updateServerContext(request, true, 0); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -519,6 +539,7 @@ func go_write_header(rh C.uintptr_t, status C.int) {
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: http: superfluous response.WriteHeader call from github.com/dunglas/frankenphp.go_write_header
|
||||
fc.responseWriter.WriteHeader(int(status))
|
||||
|
||||
if status >= 100 && status < 200 {
|
||||
|
||||
24
frankenphp.h
24
frankenphp.h
@@ -2,19 +2,27 @@
|
||||
#define _FRANKENPPHP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <Zend/zend_types.h>
|
||||
|
||||
typedef struct frankenphp_php_version {
|
||||
int major_version;
|
||||
int minor_version;
|
||||
int release_version;
|
||||
typedef struct frankenphp_version {
|
||||
unsigned char major_version;
|
||||
unsigned char minor_version;
|
||||
unsigned char release_version;
|
||||
const char *extra_version;
|
||||
const char *version;
|
||||
int version_id;
|
||||
} frankenphp_php_version;
|
||||
unsigned long version_id;
|
||||
} frankenphp_version;
|
||||
frankenphp_version frankenphp_get_version();
|
||||
|
||||
typedef struct frankenphp_config {
|
||||
frankenphp_version version;
|
||||
bool zts;
|
||||
bool zend_signals;
|
||||
bool zend_max_execution_timers;
|
||||
} frankenphp_config;
|
||||
frankenphp_config frankenphp_get_config();
|
||||
|
||||
frankenphp_php_version frankenphp_version();
|
||||
int frankenphp_check_version();
|
||||
int frankenphp_init(int num_threads);
|
||||
|
||||
int frankenphp_update_server_context(
|
||||
|
||||
@@ -109,7 +109,9 @@ func BenchmarkHelloWorld(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
|
||||
func TestHelloWorld_worker(t *testing.T) { testHelloWorld(t, &testOptions{workerScript: "index.php"}) }
|
||||
func TestHelloWorld_worker(t *testing.T) {
|
||||
testHelloWorld(t, &testOptions{workerScript: "index.php"})
|
||||
}
|
||||
func testHelloWorld(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/index.php?i=%d", i), nil)
|
||||
@@ -591,6 +593,28 @@ func testFlush(t *testing.T, opts *testOptions) {
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestTimeout_module(t *testing.T) { testTimeout(t, &testOptions{}) }
|
||||
func TestTimeout_worker(t *testing.T) {
|
||||
testTimeout(t, &testOptions{workerScript: "timeout.php"})
|
||||
}
|
||||
func testTimeout(t *testing.T, opts *testOptions) {
|
||||
config := frankenphp.Config()
|
||||
if !config.ZendMaxExecutionTimers {
|
||||
t.Skip("Zend Timer is not enabled")
|
||||
}
|
||||
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/timeout.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), fmt.Sprintf("request: %d\n<br />\n<b>Fatal error</b>: Maximum execution time of 1 second exceeded in", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
v := frankenphp.Version()
|
||||
|
||||
|
||||
2
testdata/Caddyfile
vendored
2
testdata/Caddyfile
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
debug
|
||||
frankenphp {
|
||||
worker ./phpinfo.php
|
||||
worker ./timeout.php
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
testdata/non-worker.php
vendored
Normal file
3
testdata/non-worker.php
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
frankenphp_handle_request(function () {});
|
||||
14
testdata/timeout.php
vendored
Normal file
14
testdata/timeout.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
printf("request: %d\n", $_GET['i'] ?? 'unknown');
|
||||
set_time_limit(1);
|
||||
|
||||
$x = true;
|
||||
$y = 0;
|
||||
while ($x) {
|
||||
$y++;
|
||||
}
|
||||
};
|
||||
71
worker.go
71
worker.go
@@ -43,11 +43,19 @@ func startWorkers(fileName string, nbWorkers int) error {
|
||||
for i := 0; i < nbWorkers; i++ {
|
||||
go func() {
|
||||
defer shutdownWG.Done()
|
||||
|
||||
for {
|
||||
// Create main dummy request
|
||||
r, err := http.NewRequest("GET", "", nil)
|
||||
fc := &FrankenPHPContext{
|
||||
Env: map[string]string{"SCRIPT_FILENAME": absFileName},
|
||||
}
|
||||
r, err := http.NewRequestWithContext(context.WithValue(
|
||||
context.Background(),
|
||||
contextKey,
|
||||
fc,
|
||||
), "GET", "", nil)
|
||||
|
||||
if err != nil {
|
||||
// TODO: this should never happen, maybe can we remove this block?
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
errors = append(errors, fmt.Errorf("workers %q: unable to create main worker request: %w", absFileName, err))
|
||||
@@ -55,16 +63,8 @@ func startWorkers(fileName string, nbWorkers int) error {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(
|
||||
r.Context(),
|
||||
contextKey,
|
||||
&FrankenPHPContext{
|
||||
Env: map[string]string{"SCRIPT_FILENAME": absFileName},
|
||||
},
|
||||
)
|
||||
|
||||
l.Debug("starting", zap.String("worker", absFileName))
|
||||
if err := ServeHTTP(nil, r.WithContext(ctx)); err != nil {
|
||||
if err := ServeHTTP(nil, r); err != nil {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
errors = append(errors, fmt.Errorf("workers %q: unable to start: %w", absFileName, err))
|
||||
@@ -72,6 +72,13 @@ func startWorkers(fileName string, nbWorkers int) error {
|
||||
return
|
||||
}
|
||||
|
||||
if fc.currentWorkerRequest != 0 {
|
||||
// Terminate the pending HTTP request handled by the worker
|
||||
maybeCloseContext(fc.currentWorkerRequest.Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext))
|
||||
fc.currentWorkerRequest.Delete()
|
||||
fc.currentWorkerRequest = 0
|
||||
}
|
||||
|
||||
// TODO: make the max restart configurable
|
||||
if _, ok := workersRequestChans.Load(absFileName); ok {
|
||||
workersReadyWG.Add(1)
|
||||
@@ -113,11 +120,11 @@ func go_frankenphp_worker_ready() {
|
||||
}
|
||||
|
||||
//export go_frankenphp_worker_handle_request_start
|
||||
func go_frankenphp_worker_handle_request_start(rh C.uintptr_t) C.uintptr_t {
|
||||
previousRequest := cgo.Handle(rh).Value().(*http.Request)
|
||||
previousFc := previousRequest.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t {
|
||||
mainRequest := cgo.Handle(mrh).Value().(*http.Request)
|
||||
fc := mainRequest.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
v, ok := workersRequestChans.Load(previousFc.Env["SCRIPT_FILENAME"])
|
||||
v, ok := workersRequestChans.Load(fc.Env["SCRIPT_FILENAME"])
|
||||
if !ok {
|
||||
// Probably shutting down
|
||||
return 0
|
||||
@@ -127,40 +134,48 @@ func go_frankenphp_worker_handle_request_start(rh C.uintptr_t) C.uintptr_t {
|
||||
|
||||
l := getLogger()
|
||||
|
||||
l.Debug("waiting for request", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]))
|
||||
l.Debug("waiting for request", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
|
||||
r, ok := <-rc
|
||||
if !ok {
|
||||
// channel closed, server is shutting down
|
||||
l.Debug("shutting down", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]))
|
||||
l.Debug("shutting down", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
l.Debug("request handling started", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
|
||||
if err := updateServerContext(r, false); err != nil {
|
||||
fc.currentWorkerRequest = cgo.NewHandle(r)
|
||||
|
||||
l.Debug("request handling started", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
|
||||
if err := updateServerContext(r, false, mrh); err != nil {
|
||||
// Unexpected error
|
||||
l.Debug("unexpected error", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI), zap.Error(err))
|
||||
l.Debug("unexpected error", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI), zap.Error(err))
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
return C.uintptr_t(cgo.NewHandle(r))
|
||||
return C.uintptr_t(fc.currentWorkerRequest)
|
||||
}
|
||||
|
||||
//export go_frankenphp_worker_handle_request_end
|
||||
func go_frankenphp_worker_handle_request_end(rh C.uintptr_t, deleteHandle bool) {
|
||||
if rh == 0 {
|
||||
return
|
||||
}
|
||||
//export go_frankenphp_finish_request
|
||||
func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) {
|
||||
rHandle := cgo.Handle(rh)
|
||||
r := rHandle.Value().(*http.Request)
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
if deleteHandle {
|
||||
cgo.Handle(rh).Delete()
|
||||
rHandle.Delete()
|
||||
|
||||
cgo.Handle(mrh).Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext).currentWorkerRequest = 0
|
||||
}
|
||||
|
||||
maybeCloseContext(fc)
|
||||
|
||||
fc.Logger.Debug("request handling finished", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
|
||||
var fields []zap.Field
|
||||
if mrh == 0 {
|
||||
fields = append(fields, zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
|
||||
} else {
|
||||
fields = append(fields, zap.String("url", r.RequestURI))
|
||||
}
|
||||
|
||||
fc.Logger.Debug("request handling finished", fields...)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,19 @@ func TestNonWorkerModeAlwaysWorks(t *testing.T) {
|
||||
}, &testOptions{workerScript: "phpinfo.php"})
|
||||
}
|
||||
|
||||
func TestCannotCallHandleRequestInNonWorkerMode(t *testing.T) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", "http://example.com/non-worker.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "<b>Fatal error</b>: Uncaught RuntimeException: frankenphp_handle_request() called while not in worker mode")
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func ExampleServeHTTP_workers() {
|
||||
if err := frankenphp.Init(
|
||||
frankenphp.WithWorkers("worker1.php", 4),
|
||||
|
||||
Reference in New Issue
Block a user