mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
refactor: removes context on the C side (#1404)
This commit is contained in:
committed by
GitHub
parent
09b8219ad4
commit
f50248a7d2
19
cgi.go
19
cgi.go
@@ -14,7 +14,6 @@ import "C"
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
@@ -56,7 +55,8 @@ var knownServerKeys = []string{
|
||||
//
|
||||
// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
|
||||
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
|
||||
func addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVarsArray *C.zval) {
|
||||
request := fc.request
|
||||
keys := mainThread.knownServerKeys
|
||||
// Separate remote IP and port; more lenient than net.SplitHostPort
|
||||
var ip, port string
|
||||
@@ -168,8 +168,8 @@ func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
|
||||
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
|
||||
}
|
||||
|
||||
func addHeadersToServer(request *http.Request, thread *phpThread, fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
for field, val := range request.Header {
|
||||
func addHeadersToServer(thread *phpThread, fc *frankenPHPContext, trackVarsArray *C.zval) {
|
||||
for field, val := range fc.request.Header {
|
||||
if k := mainThread.commonHeaders[field]; k != nil {
|
||||
v := strings.Join(val, ", ")
|
||||
C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
|
||||
@@ -184,7 +184,7 @@ func addHeadersToServer(request *http.Request, thread *phpThread, fc *FrankenPHP
|
||||
}
|
||||
}
|
||||
|
||||
func addPreparedEnvToServer(fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
|
||||
for k, v := range fc.env {
|
||||
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
|
||||
}
|
||||
@@ -194,11 +194,10 @@ func addPreparedEnvToServer(fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
//export go_register_variables
|
||||
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
|
||||
thread := phpThreads[threadIndex]
|
||||
r := thread.getActiveRequest()
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc := thread.getRequestContext()
|
||||
|
||||
addKnownVariablesToServer(thread, r, fc, trackVarsArray)
|
||||
addHeadersToServer(r, thread, fc, trackVarsArray)
|
||||
addKnownVariablesToServer(thread, fc, trackVarsArray)
|
||||
addHeadersToServer(thread, fc, trackVarsArray)
|
||||
|
||||
// The Prepared Environment is registered last and can overwrite any previous values
|
||||
addPreparedEnvToServer(fc, trackVarsArray)
|
||||
@@ -209,7 +208,7 @@ func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
|
||||
//
|
||||
// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
func splitPos(fc *FrankenPHPContext, path string) int {
|
||||
func splitPos(fc *frankenPHPContext, path string) int {
|
||||
if len(fc.splitPath) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
164
context.go
Normal file
164
context.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// frankenPHPContext provides contextual information about the Request to handle.
|
||||
type frankenPHPContext struct {
|
||||
documentRoot string
|
||||
splitPath []string
|
||||
env PreparedEnv
|
||||
logger *zap.Logger
|
||||
request *http.Request
|
||||
originalRequest *http.Request
|
||||
|
||||
docURI string
|
||||
pathInfo string
|
||||
scriptName string
|
||||
scriptFilename string
|
||||
|
||||
// Whether the request is already closed by us
|
||||
isDone bool
|
||||
|
||||
responseWriter http.ResponseWriter
|
||||
|
||||
done chan interface{}
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
// fromContext extracts the frankenPHPContext from a context.
|
||||
func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {
|
||||
fctx, ok = ctx.Value(contextKey).(*frankenPHPContext)
|
||||
return
|
||||
}
|
||||
|
||||
// NewRequestWithContext creates a new FrankenPHP request context.
|
||||
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
|
||||
fc := &frankenPHPContext{
|
||||
done: make(chan interface{}),
|
||||
startedAt: time.Now(),
|
||||
request: r,
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(fc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fc.logger == nil {
|
||||
fc.logger = logger
|
||||
}
|
||||
|
||||
if fc.documentRoot == "" {
|
||||
if EmbeddedAppPath != "" {
|
||||
fc.documentRoot = EmbeddedAppPath
|
||||
} else {
|
||||
var err error
|
||||
if fc.documentRoot, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fc.splitPath == nil {
|
||||
fc.splitPath = []string{".php"}
|
||||
}
|
||||
|
||||
if fc.env == nil {
|
||||
fc.env = make(map[string]string)
|
||||
}
|
||||
|
||||
if splitPos := splitPos(fc, r.URL.Path); splitPos > -1 {
|
||||
fc.docURI = r.URL.Path[:splitPos]
|
||||
fc.pathInfo = r.URL.Path[splitPos:]
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
fc.scriptName = strings.TrimSuffix(r.URL.Path, fc.pathInfo)
|
||||
|
||||
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
|
||||
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
|
||||
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
|
||||
fc.scriptName = "/" + fc.scriptName
|
||||
}
|
||||
}
|
||||
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
|
||||
|
||||
c := context.WithValue(r.Context(), contextKey, fc)
|
||||
|
||||
return r.WithContext(c), nil
|
||||
}
|
||||
|
||||
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
|
||||
r, err := http.NewRequest(http.MethodGet, requestPath, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fr, err := NewRequestWithContext(r, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fc, _ := fromContext(fr.Context())
|
||||
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
// closeContext sends the response to the client
|
||||
func (fc *frankenPHPContext) closeContext() {
|
||||
if fc.isDone {
|
||||
return
|
||||
}
|
||||
|
||||
close(fc.done)
|
||||
fc.isDone = true
|
||||
}
|
||||
|
||||
// validate checks if the request should be outright rejected
|
||||
func (fc *frankenPHPContext) validate() bool {
|
||||
if !strings.Contains(fc.request.URL.Path, "\x00") {
|
||||
return true
|
||||
}
|
||||
|
||||
fc.rejectBadRequest("Invalid request path")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (fc *frankenPHPContext) clientHasClosed() bool {
|
||||
select {
|
||||
case <-fc.request.Context().Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// reject sends a response with the given status code and message
|
||||
func (fc *frankenPHPContext) reject(statusCode int, message string) {
|
||||
if fc.isDone {
|
||||
return
|
||||
}
|
||||
|
||||
rw := fc.responseWriter
|
||||
if rw != nil {
|
||||
rw.WriteHeader(statusCode)
|
||||
_, _ = rw.Write([]byte(message))
|
||||
rw.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
fc.closeContext()
|
||||
}
|
||||
|
||||
func (fc *frankenPHPContext) rejectBadRequest(message string) {
|
||||
fc.reject(http.StatusBadRequest, message)
|
||||
}
|
||||
152
frankenphp.c
152
frankenphp.c
@@ -70,26 +70,18 @@ frankenphp_config frankenphp_get_config() {
|
||||
};
|
||||
}
|
||||
|
||||
typedef struct frankenphp_server_context {
|
||||
bool has_main_request;
|
||||
bool has_active_request;
|
||||
bool worker_ready;
|
||||
char *cookie_data;
|
||||
bool finished;
|
||||
} frankenphp_server_context;
|
||||
|
||||
bool should_filter_var = 0;
|
||||
__thread frankenphp_server_context *local_ctx = NULL;
|
||||
__thread uintptr_t thread_index;
|
||||
__thread bool is_worker_thread = false;
|
||||
__thread zval *os_environment = NULL;
|
||||
|
||||
static void frankenphp_free_request_context() {
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
if (SG(request_info).cookie_data != NULL) {
|
||||
free(SG(request_info).cookie_data);
|
||||
SG(request_info).cookie_data = NULL;
|
||||
}
|
||||
|
||||
free(ctx->cookie_data);
|
||||
ctx->cookie_data = NULL;
|
||||
|
||||
/* Is freed via thread.Unpin() */
|
||||
/* freed via thread.Unpin() */
|
||||
SG(request_info).auth_password = NULL;
|
||||
SG(request_info).auth_user = NULL;
|
||||
SG(request_info).request_method = NULL;
|
||||
@@ -159,6 +151,17 @@ static void frankenphp_worker_request_shutdown() {
|
||||
zend_set_memory_limit(PG(memory_limit));
|
||||
}
|
||||
|
||||
// shutdown the dummy request that starts the worker script
|
||||
bool frankenphp_shutdown_dummy_request(void) {
|
||||
if (SG(server_context) == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frankenphp_worker_request_shutdown();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PHPAPI void get_full_env(zval *track_vars_array) {
|
||||
go_getfullenv(thread_index, track_vars_array);
|
||||
}
|
||||
@@ -227,10 +230,6 @@ static int frankenphp_worker_request_startup() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Unfinish the request */
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
ctx->finished = false;
|
||||
|
||||
/* TODO: store the list of modules to reload in a global module variable */
|
||||
const char **module_name;
|
||||
zend_module_entry *module;
|
||||
@@ -255,20 +254,14 @@ PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (ctx->finished) {
|
||||
if (go_is_context_done(thread_index)) {
|
||||
RETURN_FALSE;
|
||||
}
|
||||
|
||||
php_output_end_all();
|
||||
php_header();
|
||||
|
||||
if (ctx->has_active_request) {
|
||||
go_frankenphp_finish_php_request(thread_index);
|
||||
}
|
||||
|
||||
ctx->finished = true;
|
||||
go_frankenphp_finish_php_request(thread_index);
|
||||
|
||||
RETURN_TRUE;
|
||||
} /* }}} */
|
||||
@@ -331,9 +324,8 @@ PHP_FUNCTION(frankenphp_request_headers) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
struct go_apache_request_headers_return headers =
|
||||
go_apache_request_headers(thread_index, ctx->has_active_request);
|
||||
go_apache_request_headers(thread_index);
|
||||
|
||||
array_init_size(return_value, headers.r1);
|
||||
|
||||
@@ -407,9 +399,7 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
Z_PARAM_FUNC(fci, fcc)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (!ctx->has_main_request) {
|
||||
if (!is_worker_thread) {
|
||||
/* not a worker, throw an error */
|
||||
zend_throw_exception(
|
||||
spl_ce_RuntimeException,
|
||||
@@ -417,22 +407,15 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
if (!ctx->worker_ready) {
|
||||
/* Clean the first dummy request created to initialize the worker */
|
||||
frankenphp_worker_request_shutdown();
|
||||
|
||||
ctx->worker_ready = true;
|
||||
}
|
||||
|
||||
#ifdef ZEND_MAX_EXECUTION_TIMERS
|
||||
/* Disable timeouts while waiting for a request to handle */
|
||||
zend_unset_timeout();
|
||||
#endif
|
||||
|
||||
bool request = go_frankenphp_worker_handle_request_start(thread_index);
|
||||
bool has_request = go_frankenphp_worker_handle_request_start(thread_index);
|
||||
if (frankenphp_worker_request_startup() == FAILURE
|
||||
/* Shutting down */
|
||||
|| !request) {
|
||||
|| !has_request) {
|
||||
RETURN_FALSE;
|
||||
}
|
||||
|
||||
@@ -445,7 +428,7 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Call the PHP func */
|
||||
/* Call the PHP func passed to frankenphp_handle_request() */
|
||||
zval retval = {0};
|
||||
fci.size = sizeof fci;
|
||||
fci.retval = &retval;
|
||||
@@ -462,7 +445,6 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
}
|
||||
|
||||
frankenphp_worker_request_shutdown();
|
||||
ctx->has_active_request = false;
|
||||
go_frankenphp_finish_worker_request(thread_index);
|
||||
|
||||
RETURN_TRUE;
|
||||
@@ -526,44 +508,25 @@ static zend_module_entry frankenphp_module = {
|
||||
STANDARD_MODULE_PROPERTIES};
|
||||
|
||||
static void frankenphp_request_shutdown() {
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (ctx->has_main_request && ctx->has_active_request) {
|
||||
frankenphp_destroy_super_globals();
|
||||
}
|
||||
|
||||
php_request_shutdown((void *)0);
|
||||
frankenphp_free_request_context();
|
||||
|
||||
memset(local_ctx, 0, sizeof(frankenphp_server_context));
|
||||
}
|
||||
|
||||
int frankenphp_update_server_context(
|
||||
bool create, bool has_main_request, bool has_active_request,
|
||||
int frankenphp_update_server_context(bool is_worker_request,
|
||||
|
||||
const char *request_method, char *query_string, zend_long content_length,
|
||||
char *path_translated, char *request_uri, const char *content_type,
|
||||
char *auth_user, char *auth_password, int proto_num) {
|
||||
frankenphp_server_context *ctx;
|
||||
const char *request_method,
|
||||
char *query_string,
|
||||
zend_long content_length,
|
||||
char *path_translated, char *request_uri,
|
||||
const char *content_type, char *auth_user,
|
||||
char *auth_password, int proto_num) {
|
||||
|
||||
if (create) {
|
||||
ctx = local_ctx;
|
||||
|
||||
ctx->worker_ready = false;
|
||||
ctx->cookie_data = NULL;
|
||||
ctx->finished = false;
|
||||
|
||||
SG(server_context) = ctx;
|
||||
} else {
|
||||
ctx = (frankenphp_server_context *)SG(server_context);
|
||||
}
|
||||
SG(server_context) = (void *)1;
|
||||
is_worker_thread = is_worker_request;
|
||||
|
||||
// It is not reset by zend engine, set it to 200.
|
||||
SG(sapi_headers).http_response_code = 200;
|
||||
|
||||
ctx->has_main_request = has_main_request;
|
||||
ctx->has_active_request = has_active_request;
|
||||
|
||||
SG(request_info).auth_password = auth_password;
|
||||
SG(request_info).auth_user = auth_user;
|
||||
SG(request_info).request_method = request_method;
|
||||
@@ -589,14 +552,6 @@ static int frankenphp_deactivate(void) {
|
||||
}
|
||||
|
||||
static size_t frankenphp_ub_write(const char *str, size_t str_length) {
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (ctx->finished) {
|
||||
/* TODO: maybe log a warning that we tried to write to a finished request?
|
||||
*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct go_ub_write_return result =
|
||||
go_ub_write(thread_index, (char *)str, str_length);
|
||||
|
||||
@@ -613,11 +568,6 @@ static int frankenphp_send_headers(sapi_headers_struct *sapi_headers) {
|
||||
}
|
||||
|
||||
int status;
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (!ctx->has_active_request) {
|
||||
return SAPI_HEADER_SEND_FAILED;
|
||||
}
|
||||
|
||||
if (SG(sapi_headers).http_status_line) {
|
||||
status = atoi((SG(sapi_headers).http_status_line) + 9);
|
||||
@@ -629,37 +579,26 @@ static int frankenphp_send_headers(sapi_headers_struct *sapi_headers) {
|
||||
}
|
||||
}
|
||||
|
||||
go_write_headers(thread_index, status, &sapi_headers->headers);
|
||||
bool success = go_write_headers(thread_index, status, &sapi_headers->headers);
|
||||
if (success) {
|
||||
return SAPI_HEADER_SENT_SUCCESSFULLY;
|
||||
}
|
||||
|
||||
return SAPI_HEADER_SENT_SUCCESSFULLY;
|
||||
return SAPI_HEADER_SEND_FAILED;
|
||||
}
|
||||
|
||||
static void frankenphp_sapi_flush(void *server_context) {
|
||||
frankenphp_server_context *ctx = (frankenphp_server_context *)server_context;
|
||||
|
||||
if (ctx && ctx->has_active_request && go_sapi_flush(thread_index)) {
|
||||
if (go_sapi_flush(thread_index)) {
|
||||
php_handle_aborted_connection();
|
||||
}
|
||||
}
|
||||
|
||||
static size_t frankenphp_read_post(char *buffer, size_t count_bytes) {
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
return ctx->has_active_request
|
||||
? go_read_post(thread_index, buffer, count_bytes)
|
||||
: 0;
|
||||
return go_read_post(thread_index, buffer, count_bytes);
|
||||
}
|
||||
|
||||
static char *frankenphp_read_cookies(void) {
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (!ctx->has_active_request) {
|
||||
return "";
|
||||
}
|
||||
|
||||
ctx->cookie_data = go_read_cookies(thread_index);
|
||||
|
||||
return ctx->cookie_data;
|
||||
return go_read_cookies(thread_index);
|
||||
}
|
||||
|
||||
/* all variables with well defined keys can safely be registered like this */
|
||||
@@ -817,10 +756,8 @@ static void frankenphp_register_variables(zval *track_vars_array) {
|
||||
* variables.
|
||||
*/
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
/* in non-worker mode we import the os environment regularly */
|
||||
if (!ctx->has_main_request) {
|
||||
if (!is_worker_thread) {
|
||||
get_full_env(track_vars_array);
|
||||
// php_import_environment_variables(track_vars_array);
|
||||
go_register_variables(thread_index, track_vars_array);
|
||||
@@ -917,8 +854,6 @@ static void *php_thread(void *arg) {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
local_ctx = malloc(sizeof(frankenphp_server_context));
|
||||
|
||||
// loop until Go signals to stop
|
||||
char *scriptName = NULL;
|
||||
while ((scriptName = go_frankenphp_before_script_execution(thread_index))) {
|
||||
@@ -932,9 +867,6 @@ static void *php_thread(void *arg) {
|
||||
|
||||
go_frankenphp_on_thread_shutdown(thread_index);
|
||||
|
||||
free(local_ctx);
|
||||
local_ctx = NULL;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
229
frankenphp.go
229
frankenphp.go
@@ -30,7 +30,6 @@ package frankenphp
|
||||
import "C"
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -42,7 +41,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -107,101 +105,6 @@ func (l syslogLevel) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// FrankenPHPContext provides contextual information about the Request to handle.
|
||||
type FrankenPHPContext struct {
|
||||
documentRoot string
|
||||
splitPath []string
|
||||
env PreparedEnv
|
||||
logger *zap.Logger
|
||||
originalRequest *http.Request
|
||||
|
||||
docURI string
|
||||
pathInfo string
|
||||
scriptName string
|
||||
scriptFilename string
|
||||
|
||||
// Whether the request is already closed by us
|
||||
closed sync.Once
|
||||
|
||||
responseWriter http.ResponseWriter
|
||||
exitStatus int
|
||||
|
||||
done chan interface{}
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func clientHasClosed(r *http.Request) bool {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequestWithContext creates a new FrankenPHP request context.
|
||||
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
|
||||
fc := &FrankenPHPContext{
|
||||
done: make(chan interface{}),
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(fc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fc.documentRoot == "" {
|
||||
if EmbeddedAppPath != "" {
|
||||
fc.documentRoot = EmbeddedAppPath
|
||||
} else {
|
||||
var err error
|
||||
if fc.documentRoot, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fc.splitPath == nil {
|
||||
fc.splitPath = []string{".php"}
|
||||
}
|
||||
|
||||
if fc.env == nil {
|
||||
fc.env = make(map[string]string)
|
||||
}
|
||||
|
||||
if fc.logger == nil {
|
||||
fc.logger = getLogger()
|
||||
}
|
||||
|
||||
if splitPos := splitPos(fc, r.URL.Path); splitPos > -1 {
|
||||
fc.docURI = r.URL.Path[:splitPos]
|
||||
fc.pathInfo = r.URL.Path[splitPos:]
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
fc.scriptName = strings.TrimSuffix(r.URL.Path, fc.pathInfo)
|
||||
|
||||
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
|
||||
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
|
||||
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
|
||||
fc.scriptName = "/" + fc.scriptName
|
||||
}
|
||||
}
|
||||
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
|
||||
|
||||
c := context.WithValue(r.Context(), contextKey, fc)
|
||||
|
||||
return r.WithContext(c), nil
|
||||
}
|
||||
|
||||
// FromContext extracts the FrankenPHPContext from a context.
|
||||
func FromContext(ctx context.Context) (fctx *FrankenPHPContext, ok bool) {
|
||||
fctx, ok = ctx.Value(contextKey).(*FrankenPHPContext)
|
||||
return
|
||||
}
|
||||
|
||||
type PHPVersion struct {
|
||||
MajorVersion int
|
||||
MinorVersion int
|
||||
@@ -343,11 +246,10 @@ func Init(options ...Option) error {
|
||||
return err
|
||||
}
|
||||
|
||||
regularRequestChan = make(chan *http.Request, totalThreadCount-workerThreadCount)
|
||||
regularRequestChan = make(chan *frankenPHPContext, totalThreadCount-workerThreadCount)
|
||||
regularThreads = make([]*phpThread, 0, totalThreadCount-workerThreadCount)
|
||||
for i := 0; i < totalThreadCount-workerThreadCount; i++ {
|
||||
thread := getInactivePHPThread()
|
||||
convertToRegularThread(thread)
|
||||
convertToRegularThread(getInactivePHPThread())
|
||||
}
|
||||
|
||||
if err := initWorkers(opt.workers); err != nil {
|
||||
@@ -389,19 +291,8 @@ func Shutdown() {
|
||||
logger.Debug("FrankenPHP shut down")
|
||||
}
|
||||
|
||||
func getLogger() *zap.Logger {
|
||||
loggerMu.RLock()
|
||||
defer loggerMu.RUnlock()
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func updateServerContext(thread *phpThread, request *http.Request, create bool, isWorkerRequest bool) error {
|
||||
fc, ok := FromContext(request.Context())
|
||||
if !ok {
|
||||
return InvalidRequestError
|
||||
}
|
||||
|
||||
func updateServerContext(thread *phpThread, fc *frankenPHPContext, isWorkerRequest bool) error {
|
||||
request := fc.request
|
||||
authUser, authPassword, ok := request.BasicAuth()
|
||||
var cAuthUser, cAuthPassword *C.char
|
||||
if ok && authPassword != "" {
|
||||
@@ -438,12 +329,9 @@ func updateServerContext(thread *phpThread, request *http.Request, create bool,
|
||||
}
|
||||
|
||||
cRequestUri := thread.pinCString(request.URL.RequestURI())
|
||||
isBootingAWorkerScript := fc.responseWriter == nil
|
||||
|
||||
ret := C.frankenphp_update_server_context(
|
||||
C.bool(create),
|
||||
C.bool(isWorkerRequest || isBootingAWorkerScript),
|
||||
C.bool(!isBootingAWorkerScript),
|
||||
C.bool(isWorkerRequest || fc.responseWriter == nil),
|
||||
|
||||
cMethod,
|
||||
cQueryString,
|
||||
@@ -469,39 +357,36 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
|
||||
return NotRunningError
|
||||
}
|
||||
|
||||
if !requestIsValid(request, responseWriter) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fc, ok := FromContext(request.Context())
|
||||
fc, ok := fromContext(request.Context())
|
||||
if !ok {
|
||||
return InvalidRequestError
|
||||
}
|
||||
|
||||
fc.responseWriter = responseWriter
|
||||
|
||||
if !fc.validate() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detect if a worker is available to handle this request
|
||||
if worker, ok := workers[fc.scriptFilename]; ok {
|
||||
worker.handleRequest(request, fc)
|
||||
worker.handleRequest(fc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If no worker was availabe send the request to non-worker threads
|
||||
handleRequestWithRegularPHPThreads(request, fc)
|
||||
handleRequestWithRegularPHPThreads(fc)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeCloseContext(fc *FrankenPHPContext) {
|
||||
fc.closed.Do(func() {
|
||||
close(fc.done)
|
||||
})
|
||||
}
|
||||
|
||||
//export go_ub_write
|
||||
func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t, C.bool) {
|
||||
r := phpThreads[threadIndex].getActiveRequest()
|
||||
fc, _ := FromContext(r.Context())
|
||||
fc := phpThreads[threadIndex].getRequestContext()
|
||||
|
||||
if fc.isDone {
|
||||
return 0, C.bool(true)
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
if fc.responseWriter == nil {
|
||||
@@ -523,28 +408,27 @@ func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t,
|
||||
fc.logger.Info(writer.(*bytes.Buffer).String())
|
||||
}
|
||||
|
||||
return C.size_t(i), C.bool(clientHasClosed(r))
|
||||
return C.size_t(i), C.bool(fc.clientHasClosed())
|
||||
}
|
||||
|
||||
//export go_apache_request_headers
|
||||
func go_apache_request_headers(threadIndex C.uintptr_t, hasActiveRequest bool) (*C.go_string, C.size_t) {
|
||||
func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
|
||||
thread := phpThreads[threadIndex]
|
||||
fc := thread.getRequestContext()
|
||||
|
||||
if !hasActiveRequest {
|
||||
if fc.responseWriter == nil {
|
||||
// worker mode, not handling a request
|
||||
mfc := thread.getActiveRequest().Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
if c := mfc.logger.Check(zapcore.DebugLevel, "apache_request_headers() called in non-HTTP context"); c != nil {
|
||||
c.Write(zap.String("worker", mfc.scriptFilename))
|
||||
if c := logger.Check(zapcore.DebugLevel, "apache_request_headers() called in non-HTTP context"); c != nil {
|
||||
c.Write(zap.String("worker", fc.scriptFilename))
|
||||
}
|
||||
|
||||
return nil, 0
|
||||
}
|
||||
r := thread.getActiveRequest()
|
||||
|
||||
headers := make([]C.go_string, 0, len(r.Header)*2)
|
||||
headers := make([]C.go_string, 0, len(fc.request.Header)*2)
|
||||
|
||||
for field, val := range r.Header {
|
||||
for field, val := range fc.request.Header {
|
||||
fd := unsafe.StringData(field)
|
||||
thread.Pin(fd)
|
||||
|
||||
@@ -562,10 +446,10 @@ func go_apache_request_headers(threadIndex C.uintptr_t, hasActiveRequest bool) (
|
||||
sd := unsafe.SliceData(headers)
|
||||
thread.Pin(sd)
|
||||
|
||||
return sd, C.size_t(len(r.Header))
|
||||
return sd, C.size_t(len(fc.request.Header))
|
||||
}
|
||||
|
||||
func addHeader(fc *FrankenPHPContext, cString *C.char, length C.int) {
|
||||
func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) {
|
||||
parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2)
|
||||
if len(parts) != 2 {
|
||||
if c := fc.logger.Check(zapcore.DebugLevel, "invalid header"); c != nil {
|
||||
@@ -579,12 +463,11 @@ func addHeader(fc *FrankenPHPContext, cString *C.char, length C.int) {
|
||||
}
|
||||
|
||||
//export go_write_headers
|
||||
func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_llist) {
|
||||
r := phpThreads[threadIndex].getActiveRequest()
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_llist) C.bool {
|
||||
fc := phpThreads[threadIndex].getRequestContext()
|
||||
|
||||
if fc.responseWriter == nil {
|
||||
return
|
||||
if fc.isDone || fc.responseWriter == nil {
|
||||
return C.bool(false)
|
||||
}
|
||||
|
||||
current := headers.head
|
||||
@@ -604,14 +487,18 @@ func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_lli
|
||||
delete(h, k)
|
||||
}
|
||||
}
|
||||
|
||||
return C.bool(true)
|
||||
}
|
||||
|
||||
//export go_sapi_flush
|
||||
func go_sapi_flush(threadIndex C.uintptr_t) bool {
|
||||
r := phpThreads[threadIndex].getActiveRequest()
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc := phpThreads[threadIndex].getRequestContext()
|
||||
if fc == nil || fc.responseWriter == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if fc.responseWriter == nil || clientHasClosed(r) {
|
||||
if fc.clientHasClosed() && !fc.isDone {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -626,13 +513,17 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool {
|
||||
|
||||
//export go_read_post
|
||||
func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes C.size_t) {
|
||||
r := phpThreads[threadIndex].getActiveRequest()
|
||||
fc := phpThreads[threadIndex].getRequestContext()
|
||||
|
||||
if fc.responseWriter == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
p := unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), countBytes)
|
||||
var err error
|
||||
for readBytes < countBytes && err == nil {
|
||||
var n int
|
||||
n, err = r.Body.Read(p[readBytes:])
|
||||
n, err = fc.request.Body.Read(p[readBytes:])
|
||||
readBytes += C.size_t(n)
|
||||
}
|
||||
|
||||
@@ -641,7 +532,7 @@ func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (r
|
||||
|
||||
//export go_read_cookies
|
||||
func go_read_cookies(threadIndex C.uintptr_t) *C.char {
|
||||
cookies := phpThreads[threadIndex].getActiveRequest().Header.Values("Cookie")
|
||||
cookies := phpThreads[threadIndex].getRequestContext().request.Header.Values("Cookie")
|
||||
cookie := strings.Join(cookies, "; ")
|
||||
if cookie == "" {
|
||||
return nil
|
||||
@@ -656,7 +547,6 @@ func go_read_cookies(threadIndex C.uintptr_t) *C.char {
|
||||
|
||||
//export go_log
|
||||
func go_log(message *C.char, level C.int) {
|
||||
l := getLogger()
|
||||
m := C.GoString(message)
|
||||
|
||||
var le syslogLevel
|
||||
@@ -668,27 +558,32 @@ func go_log(message *C.char, level C.int) {
|
||||
|
||||
switch le {
|
||||
case emerg, alert, crit, err:
|
||||
if c := l.Check(zapcore.ErrorLevel, m); c != nil {
|
||||
if c := logger.Check(zapcore.ErrorLevel, m); c != nil {
|
||||
c.Write(zap.Stringer("syslog_level", syslogLevel(level)))
|
||||
}
|
||||
|
||||
case warning:
|
||||
if c := l.Check(zapcore.WarnLevel, m); c != nil {
|
||||
if c := logger.Check(zapcore.WarnLevel, m); c != nil {
|
||||
c.Write(zap.Stringer("syslog_level", syslogLevel(level)))
|
||||
}
|
||||
|
||||
case debug:
|
||||
if c := l.Check(zapcore.DebugLevel, m); c != nil {
|
||||
if c := logger.Check(zapcore.DebugLevel, m); c != nil {
|
||||
c.Write(zap.Stringer("syslog_level", syslogLevel(level)))
|
||||
}
|
||||
|
||||
default:
|
||||
if c := l.Check(zapcore.InfoLevel, m); c != nil {
|
||||
if c := logger.Check(zapcore.InfoLevel, m); c != nil {
|
||||
c.Write(zap.Stringer("syslog_level", syslogLevel(level)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//export go_is_context_done
|
||||
func go_is_context_done(threadIndex C.uintptr_t) C.bool {
|
||||
return C.bool(phpThreads[threadIndex].getRequestContext().isDone)
|
||||
}
|
||||
|
||||
// ExecuteScriptCLI executes the PHP script passed as parameter.
|
||||
// It returns the exit status code of the script.
|
||||
func ExecuteScriptCLI(script string, args []string) int {
|
||||
@@ -716,17 +611,9 @@ func freeArgs(argv []*C.char) {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the request path does not contain null bytes
|
||||
func requestIsValid(r *http.Request, rw http.ResponseWriter) bool {
|
||||
if !strings.Contains(r.URL.Path, "\x00") {
|
||||
return true
|
||||
}
|
||||
rejectRequest(rw, "Invalid request path")
|
||||
return false
|
||||
}
|
||||
func executePHPFunction(functionName string) bool {
|
||||
cFunctionName := C.CString(functionName)
|
||||
defer C.free(unsafe.Pointer(cFunctionName))
|
||||
|
||||
func rejectRequest(rw http.ResponseWriter, message string) {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = rw.Write([]byte(message))
|
||||
rw.(http.Flusher).Flush()
|
||||
return C.frankenphp_execute_php_function(cFunctionName) == 1
|
||||
}
|
||||
|
||||
13
frankenphp.h
13
frankenphp.h
@@ -49,12 +49,15 @@ frankenphp_config frankenphp_get_config();
|
||||
int frankenphp_new_main_thread(int num_threads);
|
||||
bool frankenphp_new_php_thread(uintptr_t thread_index);
|
||||
|
||||
int frankenphp_update_server_context(
|
||||
bool create, bool has_main_request, bool has_active_request,
|
||||
bool frankenphp_shutdown_dummy_request(void);
|
||||
int frankenphp_update_server_context(bool is_worker_request,
|
||||
|
||||
const char *request_method, char *query_string, zend_long content_length,
|
||||
char *path_translated, char *request_uri, const char *content_type,
|
||||
char *auth_user, char *auth_password, int proto_num);
|
||||
const char *request_method,
|
||||
char *query_string,
|
||||
zend_long content_length,
|
||||
char *path_translated, char *request_uri,
|
||||
const char *content_type, char *auth_user,
|
||||
char *auth_password, int proto_num);
|
||||
int frankenphp_request_startup();
|
||||
int frankenphp_execute_script(char *file_name);
|
||||
|
||||
|
||||
@@ -152,6 +152,27 @@ func TestAllCommonHeadersAreCorrect(t *testing.T) {
|
||||
assert.True(t, ok, "header is not correctly capitalized: "+header)
|
||||
}
|
||||
}
|
||||
func TestFinishBootingAWorkerScript(t *testing.T) {
|
||||
logger = zap.NewNop()
|
||||
_, err := initPHPThreads(1, 1, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// boot the worker
|
||||
worker := getDummyWorker("transition-worker-1.php")
|
||||
convertToWorkerThread(phpThreads[0], worker)
|
||||
phpThreads[0].state.waitFor(stateReady)
|
||||
|
||||
assert.NotNil(t, phpThreads[0].handler.(*workerThread).dummyContext)
|
||||
assert.Nil(t, phpThreads[0].handler.(*workerThread).workerContext)
|
||||
assert.False(
|
||||
t,
|
||||
phpThreads[0].handler.(*workerThread).isBootingScript,
|
||||
"isBootingScript should be false after the worker thread is ready",
|
||||
)
|
||||
|
||||
drainPHPThreads()
|
||||
assert.Nil(t, phpThreads)
|
||||
}
|
||||
|
||||
func getDummyWorker(fileName string) *worker {
|
||||
if workers == nil {
|
||||
|
||||
19
phpthread.go
19
phpthread.go
@@ -4,7 +4,6 @@ package frankenphp
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
@@ -17,7 +16,7 @@ import (
|
||||
type phpThread struct {
|
||||
runtime.Pinner
|
||||
threadIndex int
|
||||
requestChan chan *http.Request
|
||||
requestChan chan *frankenPHPContext
|
||||
drainChan chan struct{}
|
||||
handlerMu sync.Mutex
|
||||
handler threadHandler
|
||||
@@ -30,13 +29,13 @@ type threadHandler interface {
|
||||
name() string
|
||||
beforeScriptExecution() string
|
||||
afterScriptExecution(exitStatus int)
|
||||
getActiveRequest() *http.Request
|
||||
getRequestContext() *frankenPHPContext
|
||||
}
|
||||
|
||||
func newPHPThread(threadIndex int) *phpThread {
|
||||
return &phpThread{
|
||||
threadIndex: threadIndex,
|
||||
requestChan: make(chan *http.Request),
|
||||
requestChan: make(chan *frankenPHPContext),
|
||||
state: newThreadState(),
|
||||
}
|
||||
}
|
||||
@@ -59,6 +58,7 @@ func (thread *phpThread) boot() {
|
||||
if !C.frankenphp_new_php_thread(C.uintptr_t(thread.threadIndex)) {
|
||||
logger.Panic("unable to create thread", zap.Int("threadIndex", thread.threadIndex))
|
||||
}
|
||||
|
||||
thread.state.waitFor(stateInactive)
|
||||
}
|
||||
|
||||
@@ -103,8 +103,15 @@ func (thread *phpThread) transitionToNewHandler() string {
|
||||
return thread.handler.beforeScriptExecution()
|
||||
}
|
||||
|
||||
func (thread *phpThread) getActiveRequest() *http.Request {
|
||||
return thread.handler.getActiveRequest()
|
||||
func (thread *phpThread) getRequestContext() *frankenPHPContext {
|
||||
return thread.handler.getRequestContext()
|
||||
}
|
||||
|
||||
func (thread *phpThread) name() string {
|
||||
thread.handlerMu.Lock()
|
||||
name := thread.handler.name()
|
||||
thread.handlerMu.Unlock()
|
||||
return name
|
||||
}
|
||||
|
||||
// Pin a string that is not null-terminated
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// RequestOption instances allow to configure a FrankenPHP Request.
|
||||
type RequestOption func(h *FrankenPHPContext) error
|
||||
type RequestOption func(h *frankenPHPContext) error
|
||||
|
||||
var (
|
||||
documentRootCache sync.Map
|
||||
@@ -26,7 +26,7 @@ var (
|
||||
// symlink is changed without PHP being restarted; enabling this
|
||||
// directive will set $_SERVER['DOCUMENT_ROOT'] to the real directory path.
|
||||
func WithRequestDocumentRoot(documentRoot string, resolveSymlink bool) RequestOption {
|
||||
return func(o *FrankenPHPContext) (err error) {
|
||||
return func(o *frankenPHPContext) (err error) {
|
||||
v, ok := documentRootCache.Load(documentRoot)
|
||||
if !ok {
|
||||
// make sure file root is absolute
|
||||
@@ -57,7 +57,7 @@ func WithRequestDocumentRoot(documentRoot string, resolveSymlink bool) RequestOp
|
||||
// WithRequestResolvedDocumentRoot is similar to WithRequestDocumentRoot
|
||||
// but doesn't do any checks or resolving on the path to improve performance.
|
||||
func WithRequestResolvedDocumentRoot(documentRoot string) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
return func(o *frankenPHPContext) error {
|
||||
o.documentRoot = documentRoot
|
||||
|
||||
return nil
|
||||
@@ -75,7 +75,7 @@ func WithRequestResolvedDocumentRoot(documentRoot string) RequestOption {
|
||||
// which can be mitigated with use of a try_files-like behavior
|
||||
// that 404s if the FastCGI path info is not found.
|
||||
func WithRequestSplitPath(splitPath []string) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
return func(o *frankenPHPContext) error {
|
||||
o.splitPath = splitPath
|
||||
|
||||
return nil
|
||||
@@ -100,7 +100,7 @@ func WithRequestEnv(env map[string]string) RequestOption {
|
||||
}
|
||||
|
||||
func WithRequestPreparedEnv(env PreparedEnv) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
return func(o *frankenPHPContext) error {
|
||||
o.env = env
|
||||
|
||||
return nil
|
||||
@@ -108,7 +108,7 @@ func WithRequestPreparedEnv(env PreparedEnv) RequestOption {
|
||||
}
|
||||
|
||||
func WithOriginalRequest(r *http.Request) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
return func(o *frankenPHPContext) error {
|
||||
o.originalRequest = r
|
||||
|
||||
return nil
|
||||
@@ -117,7 +117,7 @@ func WithOriginalRequest(r *http.Request) RequestOption {
|
||||
|
||||
// WithRequestLogger sets the logger associated with the current request
|
||||
func WithRequestLogger(logger *zap.Logger) RequestOption {
|
||||
return func(o *FrankenPHPContext) error {
|
||||
return func(o *frankenPHPContext) error {
|
||||
o.logger = logger
|
||||
|
||||
return nil
|
||||
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
scaleChan chan *FrankenPHPContext
|
||||
scaleChan chan *frankenPHPContext
|
||||
autoScaledThreads = []*phpThread{}
|
||||
scalingMu = new(sync.RWMutex)
|
||||
|
||||
@@ -47,7 +47,7 @@ func initAutoScaling(mainThread *phpMainThread) {
|
||||
}
|
||||
|
||||
scalingMu.Lock()
|
||||
scaleChan = make(chan *FrankenPHPContext)
|
||||
scaleChan = make(chan *frankenPHPContext)
|
||||
maxScaledThreads := mainThread.maxThreads - mainThread.numThreads
|
||||
autoScaledThreads = make([]*phpThread, 0, maxScaledThreads)
|
||||
scalingMu.Unlock()
|
||||
@@ -134,7 +134,7 @@ func scaleRegularThread() {
|
||||
autoScaledThreads = append(autoScaledThreads, thread)
|
||||
}
|
||||
|
||||
func startUpscalingThreads(maxScaledThreads int, scale chan *FrankenPHPContext, done chan struct{}) {
|
||||
func startUpscalingThreads(maxScaledThreads int, scale chan *frankenPHPContext, done chan struct{}) {
|
||||
for {
|
||||
scalingMu.Lock()
|
||||
scaledThreadCount := len(autoScaledThreads)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// representation of a thread with no work assigned to it
|
||||
// implements the threadHandler interface
|
||||
// each inactive thread weighs around ~350KB
|
||||
@@ -41,7 +37,7 @@ func (handler *inactiveThread) afterScriptExecution(exitStatus int) {
|
||||
panic("inactive threads should not execute scripts")
|
||||
}
|
||||
|
||||
func (handler *inactiveThread) getActiveRequest() *http.Request {
|
||||
func (handler *inactiveThread) getRequestContext() *frankenPHPContext {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -9,15 +8,15 @@ import (
|
||||
// executes PHP scripts in a web context
|
||||
// implements the threadHandler interface
|
||||
type regularThread struct {
|
||||
state *threadState
|
||||
thread *phpThread
|
||||
activeRequest *http.Request
|
||||
state *threadState
|
||||
thread *phpThread
|
||||
requestContext *frankenPHPContext
|
||||
}
|
||||
|
||||
var (
|
||||
regularThreads []*phpThread
|
||||
regularThreadMu = &sync.RWMutex{}
|
||||
regularRequestChan chan *http.Request
|
||||
regularRequestChan chan *frankenPHPContext
|
||||
)
|
||||
|
||||
func convertToRegularThread(thread *phpThread) {
|
||||
@@ -49,11 +48,11 @@ func (handler *regularThread) beforeScriptExecution() string {
|
||||
|
||||
// return true if the worker should continue to run
|
||||
func (handler *regularThread) afterScriptExecution(exitStatus int) {
|
||||
handler.afterRequest(exitStatus)
|
||||
handler.afterRequest()
|
||||
}
|
||||
|
||||
func (handler *regularThread) getActiveRequest() *http.Request {
|
||||
return handler.activeRequest
|
||||
func (handler *regularThread) getRequestContext() *frankenPHPContext {
|
||||
return handler.requestContext
|
||||
}
|
||||
|
||||
func (handler *regularThread) name() string {
|
||||
@@ -66,21 +65,20 @@ func (handler *regularThread) waitForRequest() string {
|
||||
|
||||
handler.state.markAsWaiting(true)
|
||||
|
||||
var r *http.Request
|
||||
var fc *frankenPHPContext
|
||||
select {
|
||||
case <-handler.thread.drainChan:
|
||||
// go back to beforeScriptExecution
|
||||
return handler.beforeScriptExecution()
|
||||
case r = <-regularRequestChan:
|
||||
case fc = <-regularRequestChan:
|
||||
}
|
||||
|
||||
handler.activeRequest = r
|
||||
handler.requestContext = fc
|
||||
handler.state.markAsWaiting(false)
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
|
||||
if err := updateServerContext(handler.thread, r, true, false); err != nil {
|
||||
rejectRequest(fc.responseWriter, err.Error())
|
||||
handler.afterRequest(0)
|
||||
if err := updateServerContext(handler.thread, fc, false); err != nil {
|
||||
fc.rejectBadRequest(err.Error())
|
||||
handler.afterRequest()
|
||||
handler.thread.Unpin()
|
||||
// go back to beforeScriptExecution
|
||||
return handler.beforeScriptExecution()
|
||||
@@ -90,17 +88,15 @@ func (handler *regularThread) waitForRequest() string {
|
||||
return fc.scriptFilename
|
||||
}
|
||||
|
||||
func (handler *regularThread) afterRequest(exitStatus int) {
|
||||
fc := handler.activeRequest.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc.exitStatus = exitStatus
|
||||
maybeCloseContext(fc)
|
||||
handler.activeRequest = nil
|
||||
func (handler *regularThread) afterRequest() {
|
||||
handler.requestContext.closeContext()
|
||||
handler.requestContext = nil
|
||||
}
|
||||
|
||||
func handleRequestWithRegularPHPThreads(r *http.Request, fc *FrankenPHPContext) {
|
||||
func handleRequestWithRegularPHPThreads(fc *frankenPHPContext) {
|
||||
metrics.StartRequest()
|
||||
select {
|
||||
case regularRequestChan <- r:
|
||||
case regularRequestChan <- fc:
|
||||
// a thread was available to handle the request immediately
|
||||
<-fc.done
|
||||
metrics.StopRequest()
|
||||
@@ -113,7 +109,7 @@ func handleRequestWithRegularPHPThreads(r *http.Request, fc *FrankenPHPContext)
|
||||
metrics.QueuedRequest()
|
||||
for {
|
||||
select {
|
||||
case regularRequestChan <- r:
|
||||
case regularRequestChan <- fc:
|
||||
metrics.DequeuedRequest()
|
||||
<-fc.done
|
||||
metrics.StopRequest()
|
||||
|
||||
107
threadworker.go
107
threadworker.go
@@ -3,7 +3,6 @@ package frankenphp
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@@ -15,13 +14,13 @@ import (
|
||||
// executes the PHP worker script in a loop
|
||||
// implements the threadHandler interface
|
||||
type workerThread struct {
|
||||
state *threadState
|
||||
thread *phpThread
|
||||
worker *worker
|
||||
fakeRequest *http.Request
|
||||
workerRequest *http.Request
|
||||
backoff *exponentialBackoff
|
||||
inRequest bool // true if the worker is currently handling a request
|
||||
state *threadState
|
||||
thread *phpThread
|
||||
worker *worker
|
||||
dummyContext *frankenPHPContext
|
||||
workerContext *frankenPHPContext
|
||||
backoff *exponentialBackoff
|
||||
isBootingScript bool // true if the worker has not reached frankenphp_handle_request yet
|
||||
}
|
||||
|
||||
func convertToWorkerThread(thread *phpThread, worker *worker) {
|
||||
@@ -63,12 +62,12 @@ func (handler *workerThread) afterScriptExecution(exitStatus int) {
|
||||
tearDownWorkerScript(handler, exitStatus)
|
||||
}
|
||||
|
||||
func (handler *workerThread) getActiveRequest() *http.Request {
|
||||
if handler.workerRequest != nil {
|
||||
return handler.workerRequest
|
||||
func (handler *workerThread) getRequestContext() *frankenPHPContext {
|
||||
if handler.workerContext != nil {
|
||||
return handler.workerContext
|
||||
}
|
||||
|
||||
return handler.fakeRequest
|
||||
return handler.dummyContext
|
||||
}
|
||||
|
||||
func (handler *workerThread) name() string {
|
||||
@@ -80,13 +79,8 @@ func setupWorkerScript(handler *workerThread, worker *worker) {
|
||||
metrics.StartWorker(worker.fileName)
|
||||
|
||||
// Create a dummy request to set up the worker
|
||||
r, err := http.NewRequest(http.MethodGet, filepath.Base(worker.fileName), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r, err = NewRequestWithContext(
|
||||
r,
|
||||
fc, err := newDummyContext(
|
||||
filepath.Base(worker.fileName),
|
||||
WithRequestDocumentRoot(filepath.Dir(worker.fileName), false),
|
||||
WithRequestPreparedEnv(worker.env),
|
||||
)
|
||||
@@ -94,11 +88,12 @@ func setupWorkerScript(handler *workerThread, worker *worker) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := updateServerContext(handler.thread, r, true, false); err != nil {
|
||||
if err := updateServerContext(handler.thread, fc, false); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
handler.fakeRequest = r
|
||||
handler.dummyContext = fc
|
||||
handler.isBootingScript = true
|
||||
clearSandboxedEnv(handler.thread)
|
||||
if c := logger.Check(zapcore.DebugLevel, "starting"); c != nil {
|
||||
c.Write(zap.String("worker", worker.fileName), zap.Int("thread", handler.thread.threadIndex))
|
||||
@@ -106,21 +101,18 @@ func setupWorkerScript(handler *workerThread, worker *worker) {
|
||||
}
|
||||
|
||||
func tearDownWorkerScript(handler *workerThread, exitStatus int) {
|
||||
worker := handler.worker
|
||||
handler.dummyContext = nil
|
||||
|
||||
// if the worker request is not nil, the script might have crashed
|
||||
// make sure to close the worker request context
|
||||
if handler.workerRequest != nil {
|
||||
fc := handler.workerRequest.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
maybeCloseContext(fc)
|
||||
handler.workerRequest = nil
|
||||
if handler.workerContext != nil {
|
||||
handler.workerContext.closeContext()
|
||||
handler.workerContext = nil
|
||||
}
|
||||
|
||||
fc := handler.fakeRequest.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc.exitStatus = exitStatus
|
||||
handler.fakeRequest = nil
|
||||
|
||||
// on exit status 0 we just run the worker script again
|
||||
worker := handler.worker
|
||||
if fc.exitStatus == 0 {
|
||||
if exitStatus == 0 {
|
||||
// TODO: make the max restart configurable
|
||||
metrics.StopWorker(worker.fileName, StopReasonRestart)
|
||||
handler.backoff.recordSuccess()
|
||||
@@ -130,11 +122,9 @@ func tearDownWorkerScript(handler *workerThread, exitStatus int) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: error status
|
||||
|
||||
// on exit status 1 we apply an exponential backoff when restarting
|
||||
metrics.StopWorker(worker.fileName, StopReasonCrash)
|
||||
if !handler.inRequest && handler.backoff.recordFailure() {
|
||||
if handler.isBootingScript && handler.backoff.recordFailure() {
|
||||
if !watcherIsEnabled {
|
||||
logger.Panic("too many consecutive worker failures", zap.String("worker", worker.fileName), zap.Int("failures", handler.backoff.failureCount))
|
||||
}
|
||||
@@ -151,7 +141,17 @@ func (handler *workerThread) waitForWorkerRequest() bool {
|
||||
c.Write(zap.String("worker", handler.worker.fileName))
|
||||
}
|
||||
|
||||
// worker threads are 'ready' only after they first reach frankenphp_handle_request()
|
||||
// Clear the first dummy request created to initialize the worker
|
||||
if handler.isBootingScript {
|
||||
handler.isBootingScript = false
|
||||
if !C.frankenphp_shutdown_dummy_request() {
|
||||
panic("Not in CGI context")
|
||||
}
|
||||
}
|
||||
|
||||
// worker threads are 'ready' after they first reach frankenphp_handle_request()
|
||||
// 'stateTransitionComplete' is only true on the first boot of the worker script,
|
||||
// while 'isBootingScript' is true on every boot of the worker script
|
||||
if handler.state.is(stateTransitionComplete) {
|
||||
metrics.ReadyWorker(handler.worker.fileName)
|
||||
handler.state.set(stateReady)
|
||||
@@ -159,7 +159,7 @@ func (handler *workerThread) waitForWorkerRequest() bool {
|
||||
|
||||
handler.state.markAsWaiting(true)
|
||||
|
||||
var r *http.Request
|
||||
var fc *frankenPHPContext
|
||||
select {
|
||||
case <-handler.thread.drainChan:
|
||||
if c := logger.Check(zapcore.DebugLevel, "shutting down"); c != nil {
|
||||
@@ -173,27 +173,24 @@ func (handler *workerThread) waitForWorkerRequest() bool {
|
||||
}
|
||||
|
||||
return false
|
||||
case r = <-handler.thread.requestChan:
|
||||
case r = <-handler.worker.requestChan:
|
||||
case fc = <-handler.thread.requestChan:
|
||||
case fc = <-handler.worker.requestChan:
|
||||
}
|
||||
|
||||
handler.workerRequest = r
|
||||
handler.workerContext = fc
|
||||
handler.state.markAsWaiting(false)
|
||||
|
||||
if c := logger.Check(zapcore.DebugLevel, "request handling started"); c != nil {
|
||||
c.Write(zap.String("worker", handler.worker.fileName), zap.String("url", r.RequestURI))
|
||||
c.Write(zap.String("worker", handler.worker.fileName), zap.String("url", fc.request.RequestURI))
|
||||
}
|
||||
handler.inRequest = true
|
||||
|
||||
if err := updateServerContext(handler.thread, r, false, true); err != nil {
|
||||
if err := updateServerContext(handler.thread, fc, true); err != nil {
|
||||
// Unexpected error or invalid request
|
||||
if c := logger.Check(zapcore.DebugLevel, "unexpected error"); c != nil {
|
||||
c.Write(zap.String("worker", handler.worker.fileName), zap.String("url", r.RequestURI), zap.Error(err))
|
||||
c.Write(zap.String("worker", handler.worker.fileName), zap.String("url", fc.request.RequestURI), zap.Error(err))
|
||||
}
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
rejectRequest(fc.responseWriter, err.Error())
|
||||
maybeCloseContext(fc)
|
||||
handler.workerRequest = nil
|
||||
fc.rejectBadRequest(err.Error())
|
||||
handler.workerContext = nil
|
||||
|
||||
return handler.waitForWorkerRequest()
|
||||
}
|
||||
@@ -214,14 +211,13 @@ func go_frankenphp_worker_handle_request_start(threadIndex C.uintptr_t) C.bool {
|
||||
//export go_frankenphp_finish_worker_request
|
||||
func go_frankenphp_finish_worker_request(threadIndex C.uintptr_t) {
|
||||
thread := phpThreads[threadIndex]
|
||||
r := thread.getActiveRequest()
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc := thread.getRequestContext()
|
||||
|
||||
maybeCloseContext(fc)
|
||||
thread.handler.(*workerThread).workerRequest = nil
|
||||
fc.closeContext()
|
||||
thread.handler.(*workerThread).workerContext = nil
|
||||
|
||||
if c := fc.logger.Check(zapcore.DebugLevel, "request handling finished"); c != nil {
|
||||
c.Write(zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI))
|
||||
c.Write(zap.String("worker", fc.scriptFilename), zap.String("url", fc.request.RequestURI))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,11 +225,10 @@ func go_frankenphp_finish_worker_request(threadIndex C.uintptr_t) {
|
||||
//
|
||||
//export go_frankenphp_finish_php_request
|
||||
func go_frankenphp_finish_php_request(threadIndex C.uintptr_t) {
|
||||
r := phpThreads[threadIndex].getActiveRequest()
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
maybeCloseContext(fc)
|
||||
fc := phpThreads[threadIndex].getRequestContext()
|
||||
fc.closeContext()
|
||||
|
||||
if c := fc.logger.Check(zapcore.DebugLevel, "request handling finished"); c != nil {
|
||||
c.Write(zap.String("url", r.RequestURI))
|
||||
c.Write(zap.String("url", fc.request.RequestURI))
|
||||
}
|
||||
}
|
||||
|
||||
13
worker.go
13
worker.go
@@ -5,7 +5,6 @@ import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -17,7 +16,7 @@ type worker struct {
|
||||
fileName string
|
||||
num int
|
||||
env PreparedEnv
|
||||
requestChan chan *http.Request
|
||||
requestChan chan *frankenPHPContext
|
||||
threads []*phpThread
|
||||
threadMutex sync.RWMutex
|
||||
}
|
||||
@@ -57,7 +56,7 @@ func initWorkers(opt []workerOpt) error {
|
||||
}
|
||||
|
||||
watcherIsEnabled = true
|
||||
if err := watcher.InitWatcher(directoriesToWatch, RestartWorkers, getLogger()); err != nil {
|
||||
if err := watcher.InitWatcher(directoriesToWatch, RestartWorkers, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -79,7 +78,7 @@ func newWorker(o workerOpt) (*worker, error) {
|
||||
fileName: absFileName,
|
||||
num: o.num,
|
||||
env: o.env,
|
||||
requestChan: make(chan *http.Request),
|
||||
requestChan: make(chan *frankenPHPContext),
|
||||
}
|
||||
workers[absFileName] = w
|
||||
|
||||
@@ -170,14 +169,14 @@ func (worker *worker) countThreads() int {
|
||||
return l
|
||||
}
|
||||
|
||||
func (worker *worker) handleRequest(r *http.Request, fc *FrankenPHPContext) {
|
||||
func (worker *worker) handleRequest(fc *frankenPHPContext) {
|
||||
metrics.StartWorkerRequest(fc.scriptFilename)
|
||||
|
||||
// dispatch requests to all worker threads in order
|
||||
worker.threadMutex.RLock()
|
||||
for _, thread := range worker.threads {
|
||||
select {
|
||||
case thread.requestChan <- r:
|
||||
case thread.requestChan <- fc:
|
||||
worker.threadMutex.RUnlock()
|
||||
<-fc.done
|
||||
metrics.StopWorkerRequest(worker.fileName, time.Since(fc.startedAt))
|
||||
@@ -192,7 +191,7 @@ func (worker *worker) handleRequest(r *http.Request, fc *FrankenPHPContext) {
|
||||
metrics.QueuedWorkerRequest(fc.scriptFilename)
|
||||
for {
|
||||
select {
|
||||
case worker.requestChan <- r:
|
||||
case worker.requestChan <- fc:
|
||||
metrics.DequeuedWorkerRequest(fc.scriptFilename)
|
||||
<-fc.done
|
||||
metrics.StopWorkerRequest(worker.fileName, time.Since(fc.startedAt))
|
||||
|
||||
Reference in New Issue
Block a user